Ver Fonte

async-imap-wasi lib added

Yurii Sokolovskyi há 3 meses atrás
pai
commit
54ed443a8a

+ 24 - 0
async-imap-wasi/.cirrus.yml

@@ -0,0 +1,24 @@
+task:
+  name: stable-x86_64-unknown-freebsd
+  freebsd_instance:
+    matrix:
+      - image: freebsd-12-0-release-amd64
+      - image: freebsd-11-2-release-amd64
+  env:
+    RUST_BACKTRACE: 1
+  setup_script:
+    - pkg install -y curl git
+    - curl https://sh.rustup.rs -sSf --output rustup.sh
+    - sh rustup.sh -y
+    - . $HOME/.cargo/env
+  check_script:
+    - . $HOME/.cargo/env
+    - cargo check --all-targets
+  build_script:
+    - . $HOME/.cargo/env
+    - cargo build --all-targets --verbose
+  test_script:
+    - . $HOME/.cargo/env
+    - cargo test --examples
+    - cargo test --doc
+    - cargo test --lib

+ 205 - 0
async-imap-wasi/CHANGELOG.md

@@ -0,0 +1,205 @@
+# Changelog 
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.9.7] - 2023-01-30
+
+- Fix parsing of METADATA results with NIL values. [#103](https://github.com/async-email/async-imap/pull/103)
+
+## [0.9.6] - 2023-01-27
+
+### Changes
+
+- Add GETMETADATA command. [#102](https://github.com/async-email/async-imap/pull/102)
+
+## [0.9.5] - 2023-12-11
+
+### Fixes
+
+- Reset IDLE timeout when keepalive is received
+
+## [0.9.4] - 2023-11-15
+
+### Fixes
+
+- Do not ignore EOF errors when reading FETCH responses. [#94](https://github.com/async-email/async-imap/pull/94)
+
+## [0.9.3] - 2023-10-20
+
+### Dependencies
+
+- Update `async-channel` to version 2.
+
+## [0.9.2] - 2023-10-20
+
+### Fixes
+
+- Fix STATUS command response parsing. [#92](https://github.com/async-email/async-imap/pull/92)
+
+## [0.9.1] - 2023-08-28
+
+### Fixes
+
+- Replace byte pool with bytes to fix memory leak. [#79](https://github.com/async-email/async-imap/pull/79)
+
+### Documentation
+
+- Remove outdated reference to `rustls.rs` example
+
+### Miscellaneous Tasks
+
+- Fix beta (1.73) clippy
+
+## [0.9.0] - 2023-06-13
+
+### Fixes
+
+- Switch from `ouroboros` to `self_cell`. [#86](https://github.com/async-email/async-imap/pull/86)
+
+  `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).
+
+## [0.8.0] - 2023-04-17
+
+### Changed
+
+- Remove `async-native-tls` dependency. TLS streams should be created by the library users as documented in `lib.rs`. #68
+- Do not generate artificial "broken pipe" errors when attempting to send a request
+  after reaching EOF on the response stream. #73
+- Do not attempt to track if the stream is closed or not.
+  `ImapStream` can wrap any kinds of streams, including streams which may become open again later,
+  like files which can be rewinded after reaching end of file or appended to.
+
+### Fixes
+
+- Update byte-pool to 0.2.4 to fix `ensure_capacity()`.
+  Previously this bug could have resulted in an attempt to read into a buffer of zero size
+  and erronous detection of the end of stream.
+
+## [0.7.0] - 2023-04-03
+
+### Added
+
+- Added changelog.
+- Add `ID` extension support.
+
+### Fixed
+
+- Fix parsing of long responses by correctly setting the `decode_needs` variable. [#74](https://github.com/async-email/async-imap/pull/74).
+
+### Changed
+
+- Make `async-native-tls` dependency optional.
+- Update to `base64` 0.21.
+
+## [0.6.0] - 2022-06-27
+
+### Added
+
+- Add `QUOTA` support.
+- Add `CONDSTORE` support: add `Session.select_condstore()`.
+- Full tokio support.
+
+### Fixed
+
+- Do not ignore `SELECT` command errors.
+
+### Changed
+
+- Replace `rental` with `ouroboros`.
+- Replace `lazy_static` with `once_cell`.
+
+## [0.5.0] - 2021-03-23
+
+### Changed
+
+- Update async-std, stop-token, migrate to stable channels.
+
+## [0.4.1] - 2020-10-14
+
+### Fixed
+
+- Fix response handling in authentication. [#36](https://github.com/async-email/async-imap/pull/36)
+
+### Changed
+
+- Update `base64` to 0.13.
+
+## [0.3.3] - 2020-08-04
+
+### Fixed
+
+- [Refactor buffering, fixing infinite loop](https://github.com/async-email/async-imap/commit/9a7097dd446784257ad9a546c6f77188e983acd6). [#33](https://github.com/async-email/async-imap/pull/33)
+- Updated `byte-pool` from 0.2.1 to 0.2.2 due to important bugfix.
+
+### Changed
+
+- [Do not try to send data when the stream is closed](https://github.com/async-email/async-imap/commit/68f21e5921a002e172d5ffadc45c62bf882a68d6).
+
+## [0.3.2] - 2020-06-11
+
+### Changed
+
+- Bump `base64` to 0.12.
+
+## [0.3.1] - 2020-05-24
+
+### Fixed
+
+- Ignore unsolicited responses if the channel is full.
+
+## [0.3.0] - 2020-05-23
+
+### Added
+
+- Make streams and futures `Send`.
+
+## [0.2.0] - 2020-01-04
+
+### Added
+
+- Added tracing logs for traffic.
+
+### Fixed
+
+- Correctly decode incomplete reads of long IMAP messages.
+- Avoid infinite loop in decoding.
+- Correct response value for manual interrupt in IDLE.
+- Handle OAuth responses without challenge.
+- Don't crash if we can't read the greeting from the server.
+- Improved handling of unsolicited responses and errors.
+
+### Changed
+
+- Use thiserror for error handling.
+
+## [0.1.1] - 2019-11-16
+
+### Fixed
+
+- Ensure there is enough space available when encoding.
+
+## 0.1.0 - 2019-11-11
+
+[0.9.7]: https://github.com/async-email/async-imap/compare/v0.9.6...v0.9.7
+[0.9.6]: https://github.com/async-email/async-imap/compare/v0.9.5...v0.9.6
+[0.9.5]: https://github.com/async-email/async-imap/compare/v0.9.4...v0.9.5
+[0.9.4]: https://github.com/async-email/async-imap/compare/v0.9.3...v0.9.4
+[0.9.3]: https://github.com/async-email/async-imap/compare/v0.9.2...v0.9.3
+[0.9.2]: https://github.com/async-email/async-imap/compare/v0.9.1...v0.9.2
+[0.9.1]: https://github.com/async-email/async-imap/compare/v0.9.0...v0.9.1
+[0.9.0]: https://github.com/async-email/async-imap/compare/v0.8.0...v0.9.0
+[0.8.0]: https://github.com/async-email/async-imap/compare/v0.7.0...v0.8.0
+[0.7.0]: https://github.com/async-email/async-imap/compare/v0.6.0...v0.7.0
+[0.6.0]: https://github.com/async-email/async-imap/compare/v0.5.0...v0.6.0
+[0.5.0]: https://github.com/async-email/async-imap/compare/v0.4.1...v0.5.0
+[0.4.1]: https://github.com/async-email/async-imap/compare/v0.4.0...v0.4.1
+[0.4.0]: https://github.com/async-email/async-imap/compare/v0.3.3...v0.4.0
+[0.3.3]: https://github.com/async-email/async-imap/compare/v0.3.2...v0.3.3
+[0.3.2]: https://github.com/async-email/async-imap/compare/v0.3.1...v0.3.2
+[0.3.1]: https://github.com/async-email/async-imap/compare/v0.3.0...v0.3.1
+[0.3.0]: https://github.com/async-email/async-imap/compare/v0.2.0...v0.3.0
+[0.2.0]: https://github.com/async-email/async-imap/compare/v0.1.1...v0.2.0
+[0.1.1]: https://github.com/async-email/async-imap/compare/v0.1.0...v0.1.1

+ 49 - 0
async-imap-wasi/Cargo.toml

@@ -0,0 +1,49 @@
+[package]
+name = "async-imap-wasi"
+version = "0.9.7"
+authors = ["dignifiedquire <me@dignifiedquire.com>"]
+documentation = "https://docs.rs/async-imap/"
+repository = "https://github.com/async-email/async-imap"
+homepage = "https://github.com/async-email/async-imap"
+description = "Async IMAP client for Rust"
+readme = "README.md"
+license = "MIT OR Apache-2.0"
+edition = "2021"
+
+keywords = ["email", "imap"]
+categories = ["email", "network-programming"]
+
+[badges]
+maintenance = { status = "actively-developed" }
+is-it-maintained-issue-resolution = { repository = "async-email/async-imap" }
+is-it-maintained-open-issues = { repository = "async-email/async-imap" }
+
+[features]
+default = ["runtime-async-std"]
+
+runtime-async-std = ["async-std"]
+runtime-tokio = ["tokio_wasi"]
+
+[dependencies]
+imap-proto = "0.16.4"
+nom = "7.0"
+base64 = "0.21"
+chrono = { version = "0.4", default-features = false, features = ["std"] }
+pin-utils = "0.1.0-alpha.4"
+futures = "0.3.15"
+self_cell = "1.0.1"
+stop-token = "0.7"
+bytes = "1"
+once_cell = "1.8.0"
+log = "0.4.8"
+thiserror = "1.0.9"
+async-channel = "2.0.0"
+
+async-std = { version = "1.8.0", default-features = false, features = ["std", "unstable"], optional = true }
+tokio_wasi = { version = "1", features = ["net", "sync", "time", "io-util"], optional = true }
+
+
+[dev-dependencies]
+pretty_assertions = "1.2"
+async-std = { version = "1.8.0", features = ["std", "attributes"] }
+tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

+ 201 - 0
async-imap-wasi/LICENSE-APACHE

@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

+ 25 - 0
async-imap-wasi/LICENSE-MIT

@@ -0,0 +1,25 @@
+Copyright (c) 2019
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.

+ 88 - 0
async-imap-wasi/README.md

@@ -0,0 +1,88 @@
+<h1 align="center">async-imap</h1>
+<div align="center">
+ <strong>
+   Async implementation of IMAP
+ </strong>
+</div>
+
+<br />
+
+<div align="center">
+  <!-- Crates version -->
+  <a href="https://crates.io/crates/async-imap">
+    <img src="https://img.shields.io/crates/v/async-imap.svg?style=flat-square"
+    alt="Crates.io version" />
+  </a>
+  <!-- Downloads -->
+  <a href="https://crates.io/crates/async-imap">
+    <img src="https://img.shields.io/crates/d/async-imap.svg?style=flat-square"
+      alt="Download" />
+  </a>
+  <!-- docs.rs docs -->
+  <a href="https://docs.rs/async-imap">
+    <img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
+      alt="docs.rs docs" />
+  </a>
+  <!-- CI -->
+  <a href="https://github.com/async-email/async-imap/actions">
+    <img src="https://github.com/async-email/async-imap/workflows/CI/badge.svg"
+      alt="CI status" />
+  </a>
+</div>
+
+<div align="center">
+  <h3>
+    <a href="https://docs.rs/async-imap">
+      API Docs
+    </a>
+    <span> | </span>
+    <a href="https://github.com/async-email/async-imap/releases">
+      Releases
+    </a>
+  </h3>
+</div>
+
+<br/>
+
+> Based on the great [rust-imap](https://crates.io/crates/imap) library.
+
+This crate lets you connect to and interact with servers that implement the IMAP protocol ([RFC
+3501](https://tools.ietf.org/html/rfc3501) and various extensions). After authenticating with
+the server, IMAP lets you list, fetch, and search for e-mails, as well as monitor mailboxes for
+changes. It supports at least the latest three stable Rust releases (possibly even older ones;
+check the [CI results](https://travis-ci.com/jonhoo/rust-imap)).
+
+To connect, use the [`connect`] function. This gives you an unauthenticated [`Client`]. You can
+then use [`Client::login`] or [`Client::authenticate`] to perform username/password or
+challenge/response authentication respectively. This in turn gives you an authenticated
+[`Session`], which lets you access the mailboxes at the server.
+
+The documentation within this crate borrows heavily from the various RFCs, but should not be
+considered a complete reference. If anything is unclear, follow the links to the RFCs embedded
+in the documentation for the various types and methods and read the raw text there!
+
+See the `examples/` directory for examples.
+
+## Running the test suite
+
+To run the integration tests, you need to have [GreenMail
+running](https://greenmail-mail-test.github.io/greenmail/#deploy_docker_standalone). The
+easiest way to do that is with Docker:
+
+```console
+$ docker pull greenmail/standalone:1.5.9
+$ 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
+```
+
+## License
+
+Licensed under either of
+ * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
+ * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
+at your option.
+
+## Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted
+for inclusion in the work by you, as defined in the Apache-2.0 license, shall
+be dual licensed as above, without any additional terms or conditions.

+ 102 - 0
async-imap-wasi/azure-pipelines.yml

@@ -0,0 +1,102 @@
+stages:
+ - stage: check
+   displayName: Compilation check
+   dependsOn: []
+   jobs:
+     - template: azure/cargo-check.yml@templates
+       parameters:
+         name: cargo_check
+ # This represents the minimum Rust version supported.
+ # Tests are not run as tests may require newer versions of rust.
+ - stage: msrv
+   displayName: "Minimum supported Rust version: 1.36.0"
+   dependsOn: []
+   jobs:
+     - template: azure/cargo-check.yml@templates
+       parameters:
+        rust: 1.36.0
+ - stage: test
+   displayName: Test suite
+   dependsOn: check
+   jobs:
+   - job: test
+     displayName: cargo test --{examples,doc,lib} (cross-platform)
+     strategy:
+       matrix:
+         Linux:
+           vmImage: ubuntu-16.04
+         MacOS:
+           vmImage: macOS-10.14
+         Windows:
+           vmImage: windows-2019
+     pool:
+       vmImage: $(vmImage)
+     steps:
+     - template: azure/install-rust.yml@templates
+       parameters:
+         rust: stable
+     - script: cargo test --examples
+       displayName: Test examples
+     - script: cargo test --doc
+       displayName: Run doctests
+     - script: cargo test --lib
+       displayName: Run unit tests
+   - job: integration
+     displayName: cargo test
+     pool:
+       vmImage: ubuntu-16.04
+     services:
+       greenmail: greenmail
+     steps:
+     - template: azure/install-rust.yml@templates
+       parameters:
+         rust: stable
+     - script: cargo test
+       displayName: Run tests
+     - template: azure/install-rust.yml@templates
+       parameters:
+         rust: beta
+     - script: cargo test
+       displayName: Run tests on beta
+     - template: azure/install-rust.yml@templates
+       parameters:
+         rust: nightly
+     - script: cargo test
+       displayName: Run tests on nightly
+       continueOnError: true
+ - stage: style
+   displayName: Style linting
+   dependsOn: check
+   jobs:
+     - template: azure/style.yml@templates
+# https://github.com/greenmail-mail-test/greenmail/issues/284
+# - stage: coverage
+#   displayName: Code coverage
+#   dependsOn: test
+#   jobs:
+#     - template: azure/coverage.yml@templates
+#       parameters:
+#         codecov_token: $(CODECOV_TOKEN_SECRET)
+#         services:
+#           greenmail: greenmail
+#         envs:
+#           TEST_HOST: greenmail
+
+resources:
+  repositories:
+    - repository: templates
+      type: github
+      name: crate-ci/azure-pipelines
+      endpoint: jonhoo
+  containers:
+   - container: greenmail
+     image: greenmail/standalone:1.5.10
+     ports:
+       - 3025:3025
+       - 3110:3110
+       - 3143:3143
+       - 3465:3465
+       - 3993:3993
+       - 3995:3995
+     env:
+       GREENMAIL_OPTS: "-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose"

+ 1 - 0
async-imap-wasi/rustfmt.toml

@@ -0,0 +1 @@
+edition = "2021"

+ 12 - 0
async-imap-wasi/src/authenticator.rs

@@ -0,0 +1,12 @@
+/// This trait allows for pluggable authentication schemes. It is used by [`Client::authenticate`] to
+/// [authenticate using SASL](https://tools.ietf.org/html/rfc3501#section-6.2.2).
+///
+/// [`Client::authenticate`]: crate::Client::authenticate
+pub trait Authenticator {
+    /// The type of the response to the challenge. This will usually be a `Vec<u8>` or `String`.
+    type Response: AsRef<[u8]>;
+
+    /// Each base64-decoded server challenge is passed to `process`.
+    /// The returned byte-string is base64-encoded and then sent back to the server.
+    fn process(&mut self, challenge: &[u8]) -> Self::Response;
+}

+ 2467 - 0
async-imap-wasi/src/client.rs

@@ -0,0 +1,2467 @@
+use std::collections::{HashMap, HashSet};
+use std::fmt;
+use std::ops::{Deref, DerefMut};
+use std::pin::Pin;
+use std::str;
+
+use async_channel::{self as channel, bounded};
+#[cfg(feature = "runtime-async-std")]
+use async_std::io::{Read, Write, WriteExt};
+use base64::Engine as _;
+use extensions::id::{format_identification, parse_id};
+use extensions::quota::parse_get_quota_root;
+use futures::{io, Stream, StreamExt};
+use imap_proto::{Metadata, RequestId, Response};
+#[cfg(feature = "runtime-tokio")]
+use tokio::io::{AsyncRead as Read, AsyncWrite as Write, AsyncWriteExt};
+
+use super::authenticator::Authenticator;
+use super::error::{Error, ParseError, Result, ValidateError};
+use super::parse::*;
+use super::types::*;
+use crate::extensions::{self, quota::parse_get_quota};
+use crate::imap_stream::ImapStream;
+
+macro_rules! quote {
+    ($x:expr) => {
+        format!("\"{}\"", $x.replace(r"\", r"\\").replace("\"", "\\\""))
+    };
+}
+
+/// An authenticated IMAP session providing the usual IMAP commands. This type is what you get from
+/// a succesful login attempt.
+///
+/// Note that the server *is* allowed to unilaterally send things to the client for messages in
+/// a selected mailbox whose status has changed. See the note on [unilateral server responses
+/// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). Any such messages are parsed out
+/// and sent on `Session::unsolicited_responses`.
+// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying
+// primitives type.
+#[derive(Debug)]
+pub struct Session<T: Read + Write + Unpin + fmt::Debug> {
+    pub(crate) conn: Connection<T>,
+    pub(crate) unsolicited_responses_tx: channel::Sender<UnsolicitedResponse>,
+
+    /// Server responses that are not related to the current command. See also the note on
+    /// [unilateral server responses in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7).
+    pub unsolicited_responses: channel::Receiver<UnsolicitedResponse>,
+}
+
+impl<T: Read + Write + Unpin + fmt::Debug> Unpin for Session<T> {}
+impl<T: Read + Write + Unpin + fmt::Debug> Unpin for Client<T> {}
+impl<T: Read + Write + Unpin + fmt::Debug> Unpin for Connection<T> {}
+
+// Make it possible to access the inner connection and modify its settings, such as read/write
+// timeouts.
+impl<T: Read + Write + Unpin + fmt::Debug> AsMut<T> for Session<T> {
+    fn as_mut(&mut self) -> &mut T {
+        self.conn.stream.as_mut()
+    }
+}
+
+/// An (unauthenticated) handle to talk to an IMAP server. This is what you get when first
+/// connecting. A succesfull call to [`Client::login`] or [`Client::authenticate`] will return a
+/// [`Session`] instance that provides the usual IMAP methods.
+// Both `Client` and `Session` deref to [`Connection`](struct.Connection.html), the underlying
+// primitives type.
+#[derive(Debug)]
+pub struct Client<T: Read + Write + Unpin + fmt::Debug> {
+    conn: Connection<T>,
+}
+
+/// The underlying primitives type. Both `Client`(unauthenticated) and `Session`(after succesful
+/// login) use a `Connection` internally for the TCP stream primitives.
+#[derive(Debug)]
+pub struct Connection<T: Read + Write + Unpin + fmt::Debug> {
+    pub(crate) stream: ImapStream<T>,
+
+    /// Manages the request ids.
+    pub(crate) request_ids: IdGenerator,
+}
+
+// `Deref` instances are so we can make use of the same underlying primitives in `Client` and
+// `Session`
+impl<T: Read + Write + Unpin + fmt::Debug> Deref for Client<T> {
+    type Target = Connection<T>;
+
+    fn deref(&self) -> &Connection<T> {
+        &self.conn
+    }
+}
+
+impl<T: Read + Write + Unpin + fmt::Debug> DerefMut for Client<T> {
+    fn deref_mut(&mut self) -> &mut Connection<T> {
+        &mut self.conn
+    }
+}
+
+impl<T: Read + Write + Unpin + fmt::Debug> Deref for Session<T> {
+    type Target = Connection<T>;
+
+    fn deref(&self) -> &Connection<T> {
+        &self.conn
+    }
+}
+
+impl<T: Read + Write + Unpin + fmt::Debug> DerefMut for Session<T> {
+    fn deref_mut(&mut self) -> &mut Connection<T> {
+        &mut self.conn
+    }
+}
+
+// As the pattern of returning the unauthenticated `Client` (a.k.a. `self`) back with a login error
+// is relatively common, it's abstacted away into a macro here.
+//
+// Note: 1) using `.map_err(|e| (e, self))` or similar here makes the closure own self, so we can't
+//          do that.
+//       2) in theory we wouldn't need the second parameter, and could just use the identifier
+//          `self` from the surrounding function, but being explicit here seems a lot cleaner.
+macro_rules! ok_or_unauth_client_err {
+    ($r:expr, $self:expr) => {
+        match $r {
+            Ok(o) => o,
+            Err(e) => return Err((e, $self)),
+        }
+    };
+}
+
+impl<T: Read + Write + Unpin + fmt::Debug + Send> Client<T> {
+    /// Creates a new client over the given stream.
+    ///
+    /// This method primarily exists for writing tests that mock the underlying transport, but can
+    /// also be used to support IMAP over custom tunnels.
+    pub fn new(stream: T) -> Client<T> {
+        let stream = ImapStream::new(stream);
+
+        Client {
+            conn: Connection {
+                stream,
+                request_ids: IdGenerator::new(),
+            },
+        }
+    }
+
+    /// Convert this Client into the raw underlying stream.
+    pub fn into_inner(self) -> T {
+        let Self { conn, .. } = self;
+        conn.into_inner()
+    }
+
+    /// Log in to the IMAP server. Upon success a [`Session`](struct.Session.html) instance is
+    /// returned; on error the original `Client` instance is returned in addition to the error.
+    /// This is because `login` takes ownership of `self`, so in order to try again (e.g. after
+    /// prompting the user for credetials), ownership of the original `Client` needs to be
+    /// transferred back to the caller.
+    ///
+    /// ```ignore
+    /// # fn main() -> async_imap::error::Result<()> {
+    /// # async_std::task::block_on(async {
+    ///
+    /// let tls = async_native_tls::TlsConnector::new();
+    /// let client = async_imap::connect(
+    ///     ("imap.example.org", 993),
+    ///     "imap.example.org",
+    ///     tls
+    /// ).await?;
+    ///
+    /// match client.login("user", "pass").await {
+    ///     Ok(s) => {
+    ///         // you are successfully authenticated!
+    ///     },
+    ///     Err((e, orig_client)) => {
+    ///         eprintln!("error logging in: {}", e);
+    ///         // prompt user and try again with orig_client here
+    ///         return Err(e);
+    ///     }
+    /// }
+    ///
+    /// # Ok(())
+    /// # }) }
+    /// ```
+    pub async fn login<U: AsRef<str>, P: AsRef<str>>(
+        mut self,
+        username: U,
+        password: P,
+    ) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
+        let u = ok_or_unauth_client_err!(validate_str(username.as_ref()), self);
+        let p = ok_or_unauth_client_err!(validate_str(password.as_ref()), self);
+        ok_or_unauth_client_err!(
+            self.run_command_and_check_ok(&format!("LOGIN {} {}", u, p), None)
+                .await,
+            self
+        );
+
+        Ok(Session::new(self.conn))
+    }
+
+    /// Authenticate with the server using the given custom `authenticator` to handle the server's
+    /// challenge.
+    ///
+    /// ```ignore
+    /// struct OAuth2 {
+    ///     user: String,
+    ///     access_token: String,
+    /// }
+    ///
+    /// impl async_imap::Authenticator for &OAuth2 {
+    ///     type Response = String;
+    ///     fn process(&mut self, _: &[u8]) -> Self::Response {
+    ///         format!(
+    ///             "user={}\x01auth=Bearer {}\x01\x01",
+    ///             self.user, self.access_token
+    ///         )
+    ///     }
+    /// }
+    ///
+    /// # fn main() -> async_imap::error::Result<()> {
+    /// # async_std::task::block_on(async {
+    ///
+    ///     let auth = OAuth2 {
+    ///         user: String::from("me@example.com"),
+    ///         access_token: String::from("<access_token>"),
+    ///     };
+    ///
+    ///     let domain = "imap.example.com";
+    ///     let tls = async_native_tls::TlsConnector::new();
+    ///     let client = async_imap::connect((domain, 993), domain, tls).await?;
+    ///     match client.authenticate("XOAUTH2", &auth).await {
+    ///         Ok(session) => {
+    ///             // you are successfully authenticated!
+    ///         },
+    ///         Err((err, orig_client)) => {
+    ///             eprintln!("error authenticating: {}", err);
+    ///             // prompt user and try again with orig_client here
+    ///             return Err(err);
+    ///         }
+    ///     };
+    /// # Ok(())
+    /// # }) }
+    /// ```
+    pub async fn authenticate<A: Authenticator, S: AsRef<str>>(
+        mut self,
+        auth_type: S,
+        authenticator: A,
+    ) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
+        let id = ok_or_unauth_client_err!(
+            self.run_command(&format!("AUTHENTICATE {}", auth_type.as_ref()))
+                .await,
+            self
+        );
+        let session = self.do_auth_handshake(id, authenticator).await?;
+        Ok(session)
+    }
+
+    /// This func does the handshake process once the authenticate command is made.
+    async fn do_auth_handshake<A: Authenticator>(
+        mut self,
+        id: RequestId,
+        mut authenticator: A,
+    ) -> ::std::result::Result<Session<T>, (Error, Client<T>)> {
+        // explicit match blocks neccessary to convert error to tuple and not bind self too
+        // early (see also comment on `login`)
+        loop {
+            if let Some(res) = self.read_response().await {
+                let res = ok_or_unauth_client_err!(res.map_err(Into::into), self);
+                match res.parsed() {
+                    Response::Continue { information, .. } => {
+                        let challenge = if let Some(text) = information {
+                            ok_or_unauth_client_err!(
+                                base64::engine::general_purpose::STANDARD
+                                    .decode(text.as_ref())
+                                    .map_err(|e| Error::Parse(ParseError::Authentication(
+                                        (*text).to_string(),
+                                        Some(e)
+                                    ))),
+                                self
+                            )
+                        } else {
+                            Vec::new()
+                        };
+                        let raw_response = &mut authenticator.process(&challenge);
+                        let auth_response =
+                            base64::engine::general_purpose::STANDARD.encode(raw_response);
+
+                        ok_or_unauth_client_err!(
+                            self.conn.run_command_untagged(&auth_response).await,
+                            self
+                        );
+                    }
+                    _ => {
+                        ok_or_unauth_client_err!(
+                            self.check_done_ok_from(&id, None, res).await,
+                            self
+                        );
+                        return Ok(Session::new(self.conn));
+                    }
+                }
+            } else {
+                return Err((Error::ConnectionLost, self));
+            }
+        }
+    }
+}
+
+impl<T: Read + Write + Unpin + fmt::Debug + Send> Session<T> {
+    unsafe_pinned!(conn: Connection<T>);
+
+    pub(crate) fn get_stream(self: Pin<&mut Self>) -> Pin<&mut ImapStream<T>> {
+        self.conn().stream()
+    }
+
+    // not public, just to avoid duplicating the channel creation code
+    fn new(conn: Connection<T>) -> Self {
+        let (tx, rx) = bounded(100);
+        Session {
+            conn,
+            unsolicited_responses: rx,
+            unsolicited_responses_tx: tx,
+        }
+    }
+
+    /// Selects a mailbox.
+    ///
+    /// The `SELECT` command selects a mailbox so that messages in the mailbox can be accessed.
+    /// Note that earlier versions of this protocol only required the FLAGS, EXISTS, and RECENT
+    /// untagged data; consequently, client implementations SHOULD implement default behavior for
+    /// missing data as discussed with the individual item.
+    ///
+    /// Only one mailbox can be selected at a time in a connection; simultaneous access to multiple
+    /// mailboxes requires multiple connections.  The `SELECT` command automatically deselects any
+    /// currently selected mailbox before attempting the new selection. Consequently, if a mailbox
+    /// is selected and a `SELECT` command that fails is attempted, no mailbox is selected.
+    ///
+    /// Note that the server *is* allowed to unilaterally send things to the client for messages in
+    /// a selected mailbox whose status has changed. See the note on [unilateral server responses
+    /// in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7). This means that if run commands,
+    /// you *may* see additional untagged `RECENT`, `EXISTS`, `FETCH`, and `EXPUNGE` responses.
+    /// You can get them from the `unsolicited_responses` channel of the [`Session`](struct.Session.html).
+    pub async fn select<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
+        // TODO: also note READ/WRITE vs READ-only mode!
+        let id = self
+            .run_command(&format!("SELECT {}", validate_str(mailbox_name.as_ref())?))
+            .await?;
+        let mbox = parse_mailbox(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+
+        Ok(mbox)
+    }
+
+    /// Selects a mailbox with `(CONDSTORE)` parameter as defined in
+    /// [RFC 7162](https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1.8).
+    pub async fn select_condstore<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
+        let id = self
+            .run_command(&format!(
+                "SELECT {} (CONDSTORE)",
+                validate_str(mailbox_name.as_ref())?
+            ))
+            .await?;
+        let mbox = parse_mailbox(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+
+        Ok(mbox)
+    }
+
+    /// The `EXAMINE` command is identical to [`Session::select`] and returns the same output;
+    /// however, the selected mailbox is identified as read-only. No changes to the permanent state
+    /// of the mailbox, including per-user state, will happen in a mailbox opened with `examine`;
+    /// in particular, messagess cannot lose [`Flag::Recent`] in an examined mailbox.
+    pub async fn examine<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<Mailbox> {
+        let id = self
+            .run_command(&format!("EXAMINE {}", validate_str(mailbox_name.as_ref())?))
+            .await?;
+        let mbox = parse_mailbox(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+
+        Ok(mbox)
+    }
+
+    /// Fetch retreives data associated with a set of messages in the mailbox.
+    ///
+    /// Note that the server *is* allowed to unilaterally include `FETCH` responses for other
+    /// messages in the selected mailbox whose status has changed. See the note on [unilateral
+    /// server responses in RFC 3501](https://tools.ietf.org/html/rfc3501#section-7).
+    ///
+    /// `query` is a list of "data items" (space-separated in parentheses if `>1`). There are three
+    /// "macro items" which specify commonly-used sets of data items, and can be used instead of
+    /// data items.  A macro must be used by itself, and not in conjunction with other macros or
+    /// data items. They are:
+    ///
+    ///  - `ALL`: equivalent to: `(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)`
+    ///  - `FAST`: equivalent to: `(FLAGS INTERNALDATE RFC822.SIZE)`
+    ///
+    /// The currently defined data items that can be fetched are listen [in the
+    /// RFC](https://tools.ietf.org/html/rfc3501#section-6.4.5), but here are some common ones:
+    ///
+    ///  - `FLAGS`: The flags that are set for this message.
+    ///  - `INTERNALDATE`: The internal date of the message.
+    ///  - `BODY[<section>]`:
+    ///
+    ///     The text of a particular body section.  The section specification is a set of zero or
+    ///     more part specifiers delimited by periods.  A part specifier is either a part number
+    ///     (see RFC) or one of the following: `HEADER`, `HEADER.FIELDS`, `HEADER.FIELDS.NOT`,
+    ///     `MIME`, and `TEXT`.  An empty section specification (i.e., `BODY[]`) refers to the
+    ///     entire message, including the header.
+    ///
+    ///     The `HEADER`, `HEADER.FIELDS`, and `HEADER.FIELDS.NOT` part specifiers refer to the
+    ///     [RFC-2822](https://tools.ietf.org/html/rfc2822) header of the message or of an
+    ///     encapsulated [MIME-IMT](https://tools.ietf.org/html/rfc2046)
+    ///     MESSAGE/[RFC822](https://tools.ietf.org/html/rfc822) message. `HEADER.FIELDS` and
+    ///     `HEADER.FIELDS.NOT` are followed by a list of field-name (as defined in
+    ///     [RFC-2822](https://tools.ietf.org/html/rfc2822)) names, and return a subset of the
+    ///     header.  The subset returned by `HEADER.FIELDS` contains only those header fields with
+    ///     a field-name that matches one of the names in the list; similarly, the subset returned
+    ///     by `HEADER.FIELDS.NOT` contains only the header fields with a non-matching field-name.
+    ///     The field-matching is case-insensitive but otherwise exact.  Subsetting does not
+    ///     exclude the [RFC-2822](https://tools.ietf.org/html/rfc2822) delimiting blank line
+    ///     between the header and the body; the blank line is included in all header fetches,
+    ///     except in the case of a message which has no body and no blank line.
+    ///
+    ///     The `MIME` part specifier refers to the [MIME-IMB](https://tools.ietf.org/html/rfc2045)
+    ///     header for this part.
+    ///
+    ///     The `TEXT` part specifier refers to the text body of the message,
+    ///     omitting the [RFC-2822](https://tools.ietf.org/html/rfc2822) header.
+    ///
+    ///     [`Flag::Seen`] is implicitly set when `BODY` is fetched; if this causes the flags to
+    ///     change, they will generally be included as part of the `FETCH` responses.
+    ///  - `BODY.PEEK[<section>]`: An alternate form of `BODY[<section>]` that does not implicitly
+    ///    set [`Flag::Seen`].
+    ///  - `ENVELOPE`: The envelope structure of the message.  This is computed by the server by
+    ///    parsing the [RFC-2822](https://tools.ietf.org/html/rfc2822) header into the component
+    ///    parts, defaulting various fields as necessary.
+    ///  - `RFC822`: Functionally equivalent to `BODY[]`.
+    ///  - `RFC822.HEADER`: Functionally equivalent to `BODY.PEEK[HEADER]`.
+    ///  - `RFC822.SIZE`: The [RFC-2822](https://tools.ietf.org/html/rfc2822) size of the message.
+    ///  - `UID`: The unique identifier for the message.
+    pub async fn fetch<S1, S2>(
+        &mut self,
+        sequence_set: S1,
+        query: S2,
+    ) -> Result<impl Stream<Item = Result<Fetch>> + '_ + Send>
+    where
+        S1: AsRef<str>,
+        S2: AsRef<str>,
+    {
+        let id = self
+            .run_command(&format!(
+                "FETCH {} {}",
+                sequence_set.as_ref(),
+                query.as_ref()
+            ))
+            .await?;
+        let res = parse_fetches(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        );
+
+        Ok(res)
+    }
+
+    /// Equivalent to [`Session::fetch`], except that all identifiers in `uid_set` are
+    /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
+    pub async fn uid_fetch<S1, S2>(
+        &mut self,
+        uid_set: S1,
+        query: S2,
+    ) -> Result<impl Stream<Item = Result<Fetch>> + '_ + Send + Unpin>
+    where
+        S1: AsRef<str>,
+        S2: AsRef<str>,
+    {
+        let id = self
+            .run_command(&format!(
+                "UID FETCH {} {}",
+                uid_set.as_ref(),
+                query.as_ref()
+            ))
+            .await?;
+        let res = parse_fetches(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        );
+        Ok(res)
+    }
+
+    /// Noop always succeeds, and it does nothing.
+    pub async fn noop(&mut self) -> Result<()> {
+        let id = self.run_command("NOOP").await?;
+        parse_noop(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+        Ok(())
+    }
+
+    /// Logout informs the server that the client is done with the connection.
+    pub async fn logout(&mut self) -> Result<()> {
+        self.run_command_and_check_ok("LOGOUT").await?;
+        Ok(())
+    }
+
+    /// The [`CREATE` command](https://tools.ietf.org/html/rfc3501#section-6.3.3) creates a mailbox
+    /// with the given name.  `Ok` is returned only if a new mailbox with that name has been
+    /// created.  It is an error to attempt to create `INBOX` or a mailbox with a name that
+    /// refers to an extant mailbox.  Any error in creation will return [`Error::No`].
+    ///
+    /// If the mailbox name is suffixed with the server's hierarchy separator character (as
+    /// returned from the server by [`Session::list`]), this is a declaration that the client
+    /// intends to create mailbox names under this name in the hierarchy.  Servers that do not
+    /// require this declaration will ignore the declaration.  In any case, the name created is
+    /// without the trailing hierarchy delimiter.
+    ///
+    /// If the server's hierarchy separator character appears elsewhere in the name, the server
+    /// will generally create any superior hierarchical names that are needed for the `CREATE`
+    /// command to be successfully completed.  In other words, an attempt to create `foo/bar/zap`
+    /// on a server in which `/` is the hierarchy separator character will usually create `foo/`
+    /// and `foo/bar/` if they do not already exist.
+    ///
+    /// If a new mailbox is created with the same name as a mailbox which was deleted, its unique
+    /// identifiers will be greater than any unique identifiers used in the previous incarnation of
+    /// the mailbox UNLESS the new incarnation has a different unique identifier validity value.
+    /// See the description of the [`UID`
+    /// command](https://tools.ietf.org/html/rfc3501#section-6.4.8) for more detail.
+    pub async fn create<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<()> {
+        self.run_command_and_check_ok(&format!("CREATE {}", validate_str(mailbox_name.as_ref())?))
+            .await?;
+
+        Ok(())
+    }
+
+    /// The [`DELETE` command](https://tools.ietf.org/html/rfc3501#section-6.3.4) permanently
+    /// removes the mailbox with the given name.  `Ok` is returned only if the mailbox has been
+    /// deleted.  It is an error to attempt to delete `INBOX` or a mailbox name that does not
+    /// exist.
+    ///
+    /// The `DELETE` command will not remove inferior hierarchical names. For example, if a mailbox
+    /// `foo` has an inferior `foo.bar` (assuming `.` is the hierarchy delimiter character),
+    /// removing `foo` will not remove `foo.bar`.  It is an error to attempt to delete a name that
+    /// has inferior hierarchical names and also has [`NameAttribute::NoSelect`].
+    ///
+    /// It is permitted to delete a name that has inferior hierarchical names and does not have
+    /// [`NameAttribute::NoSelect`].  In this case, all messages in that mailbox are removed, and
+    /// the name will acquire [`NameAttribute::NoSelect`].
+    ///
+    /// The value of the highest-used unique identifier of the deleted mailbox will be preserved so
+    /// that a new mailbox created with the same name will not reuse the identifiers of the former
+    /// incarnation, UNLESS the new incarnation has a different unique identifier validity value.
+    /// See the description of the [`UID`
+    /// command](https://tools.ietf.org/html/rfc3501#section-6.4.8) for more detail.
+    pub async fn delete<S: AsRef<str>>(&mut self, mailbox_name: S) -> Result<()> {
+        self.run_command_and_check_ok(&format!("DELETE {}", validate_str(mailbox_name.as_ref())?))
+            .await?;
+
+        Ok(())
+    }
+
+    /// The [`RENAME` command](https://tools.ietf.org/html/rfc3501#section-6.3.5) changes the name
+    /// of a mailbox.  `Ok` is returned only if the mailbox has been renamed.  It is an error to
+    /// attempt to rename from a mailbox name that does not exist or to a mailbox name that already
+    /// exists.  Any error in renaming will return [`Error::No`].
+    ///
+    /// If the name has inferior hierarchical names, then the inferior hierarchical names will also
+    /// be renamed.  For example, a rename of `foo` to `zap` will rename `foo/bar` (assuming `/` is
+    /// the hierarchy delimiter character) to `zap/bar`.
+    ///
+    /// If the server's hierarchy separator character appears in the name, the server will
+    /// generally create any superior hierarchical names that are needed for the `RENAME` command
+    /// to complete successfully.  In other words, an attempt to rename `foo/bar/zap` to
+    /// `baz/rag/zowie` on a server in which `/` is the hierarchy separator character will
+    /// generally create `baz/` and `baz/rag/` if they do not already exist.
+    ///
+    /// The value of the highest-used unique identifier of the old mailbox name will be preserved
+    /// so that a new mailbox created with the same name will not reuse the identifiers of the
+    /// former incarnation, UNLESS the new incarnation has a different unique identifier validity
+    /// value. See the description of the [`UID`
+    /// command](https://tools.ietf.org/html/rfc3501#section-6.4.8) for more detail.
+    ///
+    /// Renaming `INBOX` is permitted, and has special behavior.  It moves all messages in `INBOX`
+    /// to a new mailbox with the given name, leaving `INBOX` empty.  If the server implementation
+    /// supports inferior hierarchical names of `INBOX`, these are unaffected by a rename of
+    /// `INBOX`.
+    pub async fn rename<S1: AsRef<str>, S2: AsRef<str>>(&mut self, from: S1, to: S2) -> Result<()> {
+        self.run_command_and_check_ok(&format!(
+            "RENAME {} {}",
+            quote!(from.as_ref()),
+            quote!(to.as_ref())
+        ))
+        .await?;
+
+        Ok(())
+    }
+
+    /// The [`SUBSCRIBE` command](https://tools.ietf.org/html/rfc3501#section-6.3.6) adds the
+    /// specified mailbox name to the server's set of "active" or "subscribed" mailboxes as
+    /// returned by [`Session::lsub`].  This command returns `Ok` only if the subscription is
+    /// successful.
+    ///
+    /// The server may validate the mailbox argument to `SUBSCRIBE` to verify that it exists.
+    /// However, it will not unilaterally remove an existing mailbox name from the subscription
+    /// list even if a mailbox by that name no longer exists.
+    pub async fn subscribe<S: AsRef<str>>(&mut self, mailbox: S) -> Result<()> {
+        self.run_command_and_check_ok(&format!("SUBSCRIBE {}", quote!(mailbox.as_ref())))
+            .await?;
+        Ok(())
+    }
+
+    /// The [`UNSUBSCRIBE` command](https://tools.ietf.org/html/rfc3501#section-6.3.7) removes the
+    /// specified mailbox name from the server's set of "active" or "subscribed" mailboxes as
+    /// returned by [`Session::lsub`].  This command returns `Ok` only if the unsubscription is
+    /// successful.
+    pub async fn unsubscribe<S: AsRef<str>>(&mut self, mailbox: S) -> Result<()> {
+        self.run_command_and_check_ok(&format!("UNSUBSCRIBE {}", quote!(mailbox.as_ref())))
+            .await?;
+        Ok(())
+    }
+
+    /// The [`CAPABILITY` command](https://tools.ietf.org/html/rfc3501#section-6.1.1) requests a
+    /// listing of capabilities that the server supports.  The server will include "IMAP4rev1" as
+    /// one of the listed capabilities. See [`Capabilities`] for further details.
+    pub async fn capabilities(&mut self) -> Result<Capabilities> {
+        let id = self.run_command("CAPABILITY").await?;
+        let c = parse_capabilities(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+        Ok(c)
+    }
+
+    /// The [`EXPUNGE` command](https://tools.ietf.org/html/rfc3501#section-6.4.3) permanently
+    /// removes all messages that have [`Flag::Deleted`] set from the currently selected mailbox.
+    /// The message sequence number of each message that is removed is returned.
+    pub async fn expunge(&mut self) -> Result<impl Stream<Item = Result<Seq>> + '_ + Send> {
+        let id = self.run_command("EXPUNGE").await?;
+        let res = parse_expunge(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        );
+        Ok(res)
+    }
+
+    /// The [`UID EXPUNGE` command](https://tools.ietf.org/html/rfc4315#section-2.1) permanently
+    /// removes all messages that both have [`Flag::Deleted`] set and have a [`Uid`] that is
+    /// included in the specified sequence set from the currently selected mailbox.  If a message
+    /// either does not have [`Flag::Deleted`] set or has a [`Uid`] that is not included in the
+    /// specified sequence set, it is not affected.
+    ///
+    /// This command is particularly useful for disconnected use clients. By using `uid_expunge`
+    /// instead of [`Self::expunge`] when resynchronizing with the server, the client can ensure that it
+    /// does not inadvertantly remove any messages that have been marked as [`Flag::Deleted`] by
+    /// other clients between the time that the client was last connected and the time the client
+    /// resynchronizes.
+    ///
+    /// This command requires that the server supports [RFC
+    /// 4315](https://tools.ietf.org/html/rfc4315) as indicated by the `UIDPLUS` capability (see
+    /// [`Session::capabilities`]). If the server does not support the `UIDPLUS` capability, the
+    /// client should fall back to using [`Session::store`] to temporarily remove [`Flag::Deleted`]
+    /// from messages it does not want to remove, then invoking [`Session::expunge`].  Finally, the
+    /// client should use [`Session::store`] to restore [`Flag::Deleted`] on the messages in which
+    /// it was temporarily removed.
+    ///
+    /// Alternatively, the client may fall back to using just [`Session::expunge`], risking the
+    /// unintended removal of some messages.
+    pub async fn uid_expunge<S: AsRef<str>>(
+        &mut self,
+        uid_set: S,
+    ) -> Result<impl Stream<Item = Result<Uid>> + '_ + Send> {
+        let id = self
+            .run_command(&format!("UID EXPUNGE {}", uid_set.as_ref()))
+            .await?;
+        let res = parse_expunge(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        );
+        Ok(res)
+    }
+
+    /// The [`CHECK` command](https://tools.ietf.org/html/rfc3501#section-6.4.1) requests a
+    /// checkpoint of the currently selected mailbox.  A checkpoint refers to any
+    /// implementation-dependent housekeeping associated with the mailbox (e.g., resolving the
+    /// server's in-memory state of the mailbox with the state on its disk) that is not normally
+    /// executed as part of each command.  A checkpoint MAY take a non-instantaneous amount of real
+    /// time to complete.  If a server implementation has no such housekeeping considerations,
+    /// [`Session::check`] is equivalent to [`Session::noop`].
+    ///
+    /// There is no guarantee that an `EXISTS` untagged response will happen as a result of
+    /// `CHECK`.  [`Session::noop`] SHOULD be used for new message polling.
+    pub async fn check(&mut self) -> Result<()> {
+        self.run_command_and_check_ok("CHECK").await?;
+        Ok(())
+    }
+
+    /// The [`CLOSE` command](https://tools.ietf.org/html/rfc3501#section-6.4.2) permanently
+    /// removes all messages that have [`Flag::Deleted`] set from the currently selected mailbox,
+    /// and returns to the authenticated state from the selected state.  No `EXPUNGE` responses are
+    /// sent.
+    ///
+    /// No messages are removed, and no error is given, if the mailbox is selected by
+    /// [`Session::examine`] or is otherwise selected read-only.
+    ///
+    /// Even if a mailbox is selected, [`Session::select`], [`Session::examine`], or
+    /// [`Session::logout`] command MAY be issued without previously invoking [`Session::close`].
+    /// [`Session::select`], [`Session::examine`], and [`Session::logout`] implicitly close the
+    /// currently selected mailbox without doing an expunge.  However, when many messages are
+    /// deleted, a `CLOSE-LOGOUT` or `CLOSE-SELECT` sequence is considerably faster than an
+    /// `EXPUNGE-LOGOUT` or `EXPUNGE-SELECT` because no `EXPUNGE` responses (which the client would
+    /// probably ignore) are sent.
+    pub async fn close(&mut self) -> Result<()> {
+        self.run_command_and_check_ok("CLOSE").await?;
+        Ok(())
+    }
+
+    /// The [`STORE` command](https://tools.ietf.org/html/rfc3501#section-6.4.6) alters data
+    /// associated with a message in the mailbox.  Normally, `STORE` will return the updated value
+    /// of the data with an untagged FETCH response.  A suffix of `.SILENT` in `query` prevents the
+    /// untagged `FETCH`, and the server assumes that the client has determined the updated value
+    /// itself or does not care about the updated value.
+    ///
+    /// The currently defined data items that can be stored are:
+    ///
+    ///  - `FLAGS <flag list>`:
+    ///
+    ///    Replace the flags for the message (other than [`Flag::Recent`]) with the argument.  The
+    ///    new value of the flags is returned as if a `FETCH` of those flags was done.
+    ///
+    ///  - `FLAGS.SILENT <flag list>`: Equivalent to `FLAGS`, but without returning a new value.
+    ///
+    ///  - `+FLAGS <flag list>`
+    ///
+    ///    Add the argument to the flags for the message.  The new value of the flags is returned
+    ///    as if a `FETCH` of those flags was done.
+    ///  - `+FLAGS.SILENT <flag list>`: Equivalent to `+FLAGS`, but without returning a new value.
+    ///
+    ///  - `-FLAGS <flag list>`
+    ///
+    ///    Remove the argument from the flags for the message.  The new value of the flags is
+    ///    returned as if a `FETCH` of those flags was done.
+    ///
+    ///  - `-FLAGS.SILENT <flag list>`: Equivalent to `-FLAGS`, but without returning a new value.
+    ///
+    /// In all cases, `<flag list>` is a space-separated list enclosed in parentheses.
+    ///
+    /// # Examples
+    ///
+    /// Delete a message:
+    ///
+    /// ```no_run
+    /// use async_imap::{types::Seq, Session, error::Result};
+    /// #[cfg(feature = "runtime-async-std")]
+    /// use async_std::net::TcpStream;
+    /// #[cfg(feature = "runtime-tokio")]
+    /// use tokio::net::TcpStream;
+    /// use futures::TryStreamExt;
+    ///
+    /// async fn delete(seq: Seq, s: &mut Session<TcpStream>) -> Result<()> {
+    ///     let updates_stream = s.store(format!("{}", seq), "+FLAGS (\\Deleted)").await?;
+    ///     let _updates: Vec<_> = updates_stream.try_collect().await?;
+    ///     s.expunge().await?;
+    ///     Ok(())
+    /// }
+    /// ```
+    pub async fn store<S1, S2>(
+        &mut self,
+        sequence_set: S1,
+        query: S2,
+    ) -> Result<impl Stream<Item = Result<Fetch>> + '_ + Send>
+    where
+        S1: AsRef<str>,
+        S2: AsRef<str>,
+    {
+        let id = self
+            .run_command(&format!(
+                "STORE {} {}",
+                sequence_set.as_ref(),
+                query.as_ref()
+            ))
+            .await?;
+        let res = parse_fetches(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        );
+        Ok(res)
+    }
+
+    /// Equivalent to [`Session::store`], except that all identifiers in `sequence_set` are
+    /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
+    pub async fn uid_store<S1, S2>(
+        &mut self,
+        uid_set: S1,
+        query: S2,
+    ) -> Result<impl Stream<Item = Result<Fetch>> + '_ + Send>
+    where
+        S1: AsRef<str>,
+        S2: AsRef<str>,
+    {
+        let id = self
+            .run_command(&format!(
+                "UID STORE {} {}",
+                uid_set.as_ref(),
+                query.as_ref()
+            ))
+            .await?;
+        let res = parse_fetches(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        );
+        Ok(res)
+    }
+
+    /// The [`COPY` command](https://tools.ietf.org/html/rfc3501#section-6.4.7) copies the
+    /// specified message(s) to the end of the specified destination mailbox.  The flags and
+    /// internal date of the message(s) will generally be preserved, and [`Flag::Recent`] will
+    /// generally be set, in the copy.
+    ///
+    /// If the `COPY` command is unsuccessful for any reason, the server restores the destination
+    /// mailbox to its state before the `COPY` attempt.
+    pub async fn copy<S1: AsRef<str>, S2: AsRef<str>>(
+        &mut self,
+        sequence_set: S1,
+        mailbox_name: S2,
+    ) -> Result<()> {
+        self.run_command_and_check_ok(&format!(
+            "COPY {} {}",
+            sequence_set.as_ref(),
+            mailbox_name.as_ref()
+        ))
+        .await?;
+
+        Ok(())
+    }
+
+    /// Equivalent to [`Session::copy`], except that all identifiers in `sequence_set` are
+    /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
+    pub async fn uid_copy<S1: AsRef<str>, S2: AsRef<str>>(
+        &mut self,
+        uid_set: S1,
+        mailbox_name: S2,
+    ) -> Result<()> {
+        self.run_command_and_check_ok(&format!(
+            "UID COPY {} {}",
+            uid_set.as_ref(),
+            mailbox_name.as_ref()
+        ))
+        .await?;
+
+        Ok(())
+    }
+
+    /// The [`MOVE` command](https://tools.ietf.org/html/rfc6851#section-3.1) takes two
+    /// arguments: a sequence set and a named mailbox. Each message included in the set is moved,
+    /// rather than copied, from the selected (source) mailbox to the named (target) mailbox.
+    ///
+    /// This means that a new message is created in the target mailbox with a
+    /// new [`Uid`], the original message is removed from the source mailbox, and
+    /// it appears to the client as a single action.  This has the same
+    /// effect for each message as this sequence:
+    ///
+    ///   1. COPY
+    ///   2. STORE +FLAGS.SILENT \DELETED
+    ///   3. EXPUNGE
+    ///
+    /// This command requires that the server supports [RFC
+    /// 6851](https://tools.ietf.org/html/rfc6851) as indicated by the `MOVE` capability (see
+    /// [`Session::capabilities`]).
+    ///
+    /// Although the effect of the `MOVE` is the same as the preceding steps, the semantics are not
+    /// identical: The intermediate states produced by those steps do not occur, and the response
+    /// codes are different.  In particular, though the `COPY` and `EXPUNGE` response codes will be
+    /// returned, response codes for a `store` will not be generated and [`Flag::Deleted`] will not
+    /// be set for any message.
+    ///
+    /// Because a `MOVE` applies to a set of messages, it might fail partway through the set.
+    /// Regardless of whether the command is successful in moving the entire set, each individual
+    /// message will either be moved or unaffected.  The server will leave each message in a state
+    /// where it is in at least one of the source or target mailboxes (no message can be lost or
+    /// orphaned).  The server will generally not leave any message in both mailboxes (it would be
+    /// bad for a partial failure to result in a bunch of duplicate messages).  This is true even
+    /// if the server returns with [`Error::No`].
+    pub async fn mv<S1: AsRef<str>, S2: AsRef<str>>(
+        &mut self,
+        sequence_set: S1,
+        mailbox_name: S2,
+    ) -> Result<()> {
+        self.run_command_and_check_ok(&format!(
+            "MOVE {} {}",
+            sequence_set.as_ref(),
+            validate_str(mailbox_name.as_ref())?
+        ))
+        .await?;
+
+        Ok(())
+    }
+
+    /// Equivalent to [`Session::copy`], except that all identifiers in `sequence_set` are
+    /// [`Uid`]s. See also the [`UID` command](https://tools.ietf.org/html/rfc3501#section-6.4.8)
+    /// and the [semantics of `MOVE` and `UID
+    /// MOVE`](https://tools.ietf.org/html/rfc6851#section-3.3).
+    pub async fn uid_mv<S1: AsRef<str>, S2: AsRef<str>>(
+        &mut self,
+        uid_set: S1,
+        mailbox_name: S2,
+    ) -> Result<()> {
+        self.run_command_and_check_ok(&format!(
+            "UID MOVE {} {}",
+            uid_set.as_ref(),
+            validate_str(mailbox_name.as_ref())?
+        ))
+        .await?;
+
+        Ok(())
+    }
+
+    /// The [`LIST` command](https://tools.ietf.org/html/rfc3501#section-6.3.8) returns a subset of
+    /// names from the complete set of all names available to the client.  It returns the name
+    /// attributes, hierarchy delimiter, and name of each such name; see [`Name`] for more detail.
+    ///
+    /// If `reference_name` is `None` (or `""`), the currently selected mailbox is used.
+    /// The returned mailbox names must match the supplied `mailbox_pattern`.  A non-empty
+    /// reference name argument is the name of a mailbox or a level of mailbox hierarchy, and
+    /// indicates the context in which the mailbox name is interpreted.
+    ///
+    /// If `mailbox_pattern` is `None` (or `""`), it is a special request to return the hierarchy
+    /// delimiter and the root name of the name given in the reference.  The value returned as the
+    /// root MAY be the empty string if the reference is non-rooted or is an empty string.  In all
+    /// cases, a hierarchy delimiter (or `NIL` if there is no hierarchy) is returned.  This permits
+    /// a client to get the hierarchy delimiter (or find out that the mailbox names are flat) even
+    /// when no mailboxes by that name currently exist.
+    ///
+    /// The reference and mailbox name arguments are interpreted into a canonical form that
+    /// represents an unambiguous left-to-right hierarchy.  The returned mailbox names will be in
+    /// the interpreted form.
+    ///
+    /// The character `*` is a wildcard, and matches zero or more characters at this position.  The
+    /// character `%` is similar to `*`, but it does not match a hierarchy delimiter.  If the `%`
+    /// wildcard is the last character of a mailbox name argument, matching levels of hierarchy are
+    /// also returned.  If these levels of hierarchy are not also selectable mailboxes, they are
+    /// returned with [`NameAttribute::NoSelect`].
+    ///
+    /// The special name `INBOX` is included if `INBOX` is supported by this server for this user
+    /// and if the uppercase string `INBOX` matches the interpreted reference and mailbox name
+    /// arguments with wildcards.  The criteria for omitting `INBOX` is whether `SELECT INBOX` will
+    /// return failure; it is not relevant whether the user's real `INBOX` resides on this or some
+    /// other server.
+    pub async fn list(
+        &mut self,
+        reference_name: Option<&str>,
+        mailbox_pattern: Option<&str>,
+    ) -> Result<impl Stream<Item = Result<Name>> + '_ + Send> {
+        let id = self
+            .run_command(&format!(
+                "LIST {} {}",
+                quote!(reference_name.unwrap_or("")),
+                mailbox_pattern.unwrap_or("\"\"")
+            ))
+            .await?;
+
+        Ok(parse_names(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        ))
+    }
+
+    /// The [`LSUB` command](https://tools.ietf.org/html/rfc3501#section-6.3.9) returns a subset of
+    /// names from the set of names that the user has declared as being "active" or "subscribed".
+    /// The arguments to this method the same as for [`Session::list`].
+    ///
+    /// The returned [`Name`]s MAY contain different mailbox flags from response to
+    /// [`Session::list`].  If this should happen, the flags returned by [`Session::list`] are
+    /// considered more authoritative.
+    ///
+    /// A special situation occurs when invoking `lsub` with the `%` wildcard. Consider what
+    /// happens if `foo/bar` (with a hierarchy delimiter of `/`) is subscribed but `foo` is not.  A
+    /// `%` wildcard to `lsub` must return `foo`, not `foo/bar`, and it will be flagged with
+    /// [`NameAttribute::NoSelect`].
+    ///
+    /// The server will not unilaterally remove an existing mailbox name from the subscription list
+    /// even if a mailbox by that name no longer exists.
+    pub async fn lsub(
+        &mut self,
+        reference_name: Option<&str>,
+        mailbox_pattern: Option<&str>,
+    ) -> Result<impl Stream<Item = Result<Name>> + '_ + Send> {
+        let id = self
+            .run_command(&format!(
+                "LSUB {} {}",
+                quote!(reference_name.unwrap_or("")),
+                mailbox_pattern.unwrap_or("")
+            ))
+            .await?;
+        let names = parse_names(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        );
+
+        Ok(names)
+    }
+
+    /// The [`STATUS` command](https://tools.ietf.org/html/rfc3501#section-6.3.10) requests the
+    /// status of the indicated mailbox. It does not change the currently selected mailbox, nor
+    /// does it affect the state of any messages in the queried mailbox (in particular, `status`
+    /// will not cause messages to lose [`Flag::Recent`]).
+    ///
+    /// `status` provides an alternative to opening a second [`Session`] and using
+    /// [`Session::examine`] on a mailbox to query that mailbox's status without deselecting the
+    /// current mailbox in the first `Session`.
+    ///
+    /// Unlike [`Session::list`], `status` is not guaranteed to be fast in its response.  Under
+    /// certain circumstances, it can be quite slow.  In some implementations, the server is
+    /// obliged to open the mailbox read-only internally to obtain certain status information.
+    /// Also unlike [`Session::list`], `status` does not accept wildcards.
+    ///
+    /// > Note: `status` is intended to access the status of mailboxes other than the currently
+    /// > selected mailbox.  Because `status` can cause the mailbox to be opened internally, and
+    /// > because this information is available by other means on the selected mailbox, `status`
+    /// > SHOULD NOT be used on the currently selected mailbox.
+    ///
+    /// The STATUS command MUST NOT be used as a "check for new messages in the selected mailbox"
+    /// operation (refer to sections [7](https://tools.ietf.org/html/rfc3501#section-7),
+    /// [7.3.1](https://tools.ietf.org/html/rfc3501#section-7.3.1), and
+    /// [7.3.2](https://tools.ietf.org/html/rfc3501#section-7.3.2) for more information about the
+    /// proper method for new message checking).
+    ///
+    /// The currently defined status data items that can be requested are:
+    ///
+    ///  - `MESSAGES`: The number of messages in the mailbox.
+    ///  - `RECENT`: The number of messages with [`Flag::Recent`] set.
+    ///  - `UIDNEXT`: The next [`Uid`] of the mailbox.
+    ///  - `UIDVALIDITY`: The unique identifier validity value of the mailbox (see [`Uid`]).
+    ///  - `UNSEEN`: The number of messages which do not have [`Flag::Seen`] set.
+    ///
+    /// `data_items` is a space-separated list enclosed in parentheses.
+    pub async fn status<S1: AsRef<str>, S2: AsRef<str>>(
+        &mut self,
+        mailbox_name: S1,
+        data_items: S2,
+    ) -> Result<Mailbox> {
+        let id = self
+            .run_command(&format!(
+                "STATUS {} {}",
+                validate_str(mailbox_name.as_ref())?,
+                data_items.as_ref()
+            ))
+            .await?;
+        let mbox = parse_status(
+            &mut self.conn.stream,
+            mailbox_name.as_ref(),
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+        Ok(mbox)
+    }
+
+    /// This method returns a handle that lets you use the [`IDLE`
+    /// command](https://tools.ietf.org/html/rfc2177#section-3) to listen for changes to the
+    /// currently selected mailbox.
+    ///
+    /// It's often more desirable to have the server transmit updates to the client in real time.
+    /// This allows a user to see new mail immediately.  It also helps some real-time applications
+    /// based on IMAP, which might otherwise need to poll extremely often (such as every few
+    /// seconds).  While the spec actually does allow a server to push `EXISTS` responses
+    /// aysynchronously, a client can't expect this behaviour and must poll.  This method provides
+    /// you with such a mechanism.
+    ///
+    /// `idle` may be used with any server that returns `IDLE` as one of the supported capabilities
+    /// (see [`Session::capabilities`]). If the server does not advertise the `IDLE` capability,
+    /// the client MUST NOT use `idle` and must instead poll for mailbox updates.  In particular,
+    /// the client MUST continue to be able to accept unsolicited untagged responses to ANY
+    /// command, as specified in the base IMAP specification.
+    ///
+    /// See [`extensions::idle::Handle`] for details.
+    pub fn idle(self) -> extensions::idle::Handle<T> {
+        extensions::idle::Handle::new(self)
+    }
+
+    /// The [`APPEND` command](https://tools.ietf.org/html/rfc3501#section-6.3.11) appends
+    /// `content` as a new message to the end of the specified destination `mailbox`.  This
+    /// argument SHOULD be in the format of an [RFC-2822](https://tools.ietf.org/html/rfc2822)
+    /// message.
+    ///
+    /// > Note: There MAY be exceptions, e.g., draft messages, in which required RFC-2822 header
+    /// > lines are omitted in the message literal argument to `append`.  The full implications of
+    /// > doing so MUST be understood and carefully weighed.
+    ///
+    /// If the append is unsuccessful for any reason, the mailbox is restored to its state before
+    /// the append attempt; no partial appending will happen.
+    ///
+    /// If the destination `mailbox` does not exist, the server returns an error, and does not
+    /// automatically create the mailbox.
+    ///
+    /// If the mailbox is currently selected, the normal new message actions will generally occur.
+    /// Specifically, the server will generally notify the client immediately via an untagged
+    /// `EXISTS` response.  If the server does not do so, the client MAY issue a `NOOP` command (or
+    /// failing that, a `CHECK` command) after one or more `APPEND` commands.
+    pub async fn append<S: AsRef<str>, B: AsRef<[u8]>>(
+        &mut self,
+        mailbox: S,
+        content: B,
+    ) -> Result<()> {
+        let content = content.as_ref();
+        self.run_command(&format!(
+            "APPEND \"{}\" {{{}}}",
+            mailbox.as_ref(),
+            content.len()
+        ))
+        .await?;
+
+        match self.read_response().await {
+            Some(Ok(res)) => {
+                if let Response::Continue { .. } = res.parsed() {
+                    self.stream.as_mut().write_all(content).await?;
+                    self.stream.as_mut().write_all(b"\r\n").await?;
+                    self.stream.flush().await?;
+                    self.read_response().await.transpose()?;
+                    Ok(())
+                } else {
+                    Err(Error::Append)
+                }
+            }
+            Some(Err(err)) => Err(err.into()),
+            _ => Err(Error::Append),
+        }
+    }
+
+    /// The [`SEARCH` command](https://tools.ietf.org/html/rfc3501#section-6.4.4) searches the
+    /// mailbox for messages that match the given `query`.  `query` consist of one or more search
+    /// keys separated by spaces.  The response from the server contains a listing of [`Seq`]s
+    /// corresponding to those messages that match the searching criteria.
+    ///
+    /// When multiple search keys are specified, the result is the intersection of all the messages
+    /// that match those keys.  Or, in other words, only messages that match *all* the keys. For
+    /// example, the criteria
+    ///
+    /// ```text
+    /// DELETED FROM "SMITH" SINCE 1-Feb-1994
+    /// ```
+    ///
+    /// refers to all deleted messages from Smith that were placed in the mailbox since February 1,
+    /// 1994.  A search key can also be a parenthesized list of one or more search keys (e.g., for
+    /// use with the `OR` and `NOT` keys).
+    ///
+    /// In all search keys that use strings, a message matches the key if the string is a substring
+    /// of the field.  The matching is case-insensitive.
+    ///
+    /// Below is a selection of common search keys.  The full list can be found in the
+    /// specification of the [`SEARCH command`](https://tools.ietf.org/html/rfc3501#section-6.4.4).
+    ///
+    ///  - `NEW`: Messages that have [`Flag::Recent`] set but not [`Flag::Seen`]. This is functionally equivalent to `(RECENT UNSEEN)`.
+    ///  - `OLD`: Messages that do not have [`Flag::Recent`] set.  This is functionally equivalent to `NOT RECENT` (as opposed to `NOT NEW`).
+    ///  - `RECENT`: Messages that have [`Flag::Recent`] set.
+    ///  - `ANSWERED`: Messages with [`Flag::Answered`] set.
+    ///  - `DELETED`: Messages with [`Flag::Deleted`] set.
+    ///  - `DRAFT`: Messages with [`Flag::Draft`] set.
+    ///  - `FLAGGED`: Messages with [`Flag::Flagged`] set.
+    ///  - `SEEN`: Messages that have [`Flag::Seen`] set.
+    ///  - `<sequence set>`: Messages with message sequence numbers corresponding to the specified message sequence number set.
+    ///  - `UID <sequence set>`: Messages with [`Uid`] corresponding to the specified unique identifier set.  Sequence set ranges are permitted.
+    ///
+    ///  - `SUBJECT <string>`: Messages that contain the specified string in the envelope structure's `SUBJECT` field.
+    ///  - `BODY <string>`: Messages that contain the specified string in the body of the message.
+    ///  - `FROM <string>`: Messages that contain the specified string in the envelope structure's `FROM` field.
+    ///  - `TO <string>`: Messages that contain the specified string in the envelope structure's `TO` field.
+    ///
+    ///  - `NOT <search-key>`: Messages that do not match the specified search key.
+    ///  - `OR <search-key1> <search-key2>`: Messages that match either search key.
+    ///
+    ///  - `BEFORE <date>`: Messages whose internal date (disregarding time and timezone) is earlier than the specified date.
+    ///  - `SINCE <date>`: Messages whose internal date (disregarding time and timezone) is within or later than the specified date.
+    pub async fn search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Seq>> {
+        let id = self
+            .run_command(&format!("SEARCH {}", query.as_ref()))
+            .await?;
+        let seqs = parse_ids(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+
+        Ok(seqs)
+    }
+
+    /// Equivalent to [`Session::search`], except that the returned identifiers
+    /// are [`Uid`] instead of [`Seq`]. See also the [`UID`
+    /// command](https://tools.ietf.org/html/rfc3501#section-6.4.8).
+    pub async fn uid_search<S: AsRef<str>>(&mut self, query: S) -> Result<HashSet<Uid>> {
+        let id = self
+            .run_command(&format!("UID SEARCH {}", query.as_ref()))
+            .await?;
+        let uids = parse_ids(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+
+        Ok(uids)
+    }
+
+    /// The [`GETQUOTA` command](https://tools.ietf.org/html/rfc2087#section-4.2)
+    pub async fn get_quota(&mut self, quota_root: &str) -> Result<Quota> {
+        let id = self
+            .run_command(format!("GETQUOTA {}", quote!(quota_root)))
+            .await?;
+        let c = parse_get_quota(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+        Ok(c)
+    }
+
+    /// The [`GETQUOTAROOT` command](https://tools.ietf.org/html/rfc2087#section-4.3)
+    pub async fn get_quota_root(
+        &mut self,
+        mailbox_name: &str,
+    ) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
+        let id = self
+            .run_command(format!("GETQUOTAROOT {}", quote!(mailbox_name)))
+            .await?;
+        let c = parse_get_quota_root(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+        Ok(c)
+    }
+
+    /// The [`GETMETADATA` command](https://datatracker.ietf.org/doc/html/rfc5464.html#section-4.2)
+    pub async fn get_metadata(
+        &mut self,
+        mailbox_name: &str,
+        options: &str,
+        entry_specifier: &str,
+    ) -> Result<Vec<Metadata>> {
+        let options = if options.is_empty() {
+            String::new()
+        } else {
+            format!(" {options}")
+        };
+        let id = self
+            .run_command(format!(
+                "GETMETADATA {} {}{}",
+                quote!(mailbox_name),
+                options,
+                entry_specifier
+            ))
+            .await?;
+        let metadata = parse_metadata(
+            &mut self.conn.stream,
+            mailbox_name,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+        Ok(metadata)
+    }
+
+    /// The [`ID` command](https://datatracker.ietf.org/doc/html/rfc2971)
+    ///
+    /// `identification` is an iterable sequence of pairs such as `("name", Some("MyMailClient"))`.
+    pub async fn id(
+        &mut self,
+        identification: impl IntoIterator<Item = (&str, Option<&str>)>,
+    ) -> Result<Option<HashMap<String, String>>> {
+        let id = self
+            .run_command(format!("ID ({})", format_identification(identification)))
+            .await?;
+        let server_identification = parse_id(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+        Ok(server_identification)
+    }
+
+    /// Similar to `id`, but don't identify ourselves.
+    ///
+    /// Sends `ID NIL` command and returns server response.
+    pub async fn id_nil(&mut self) -> Result<Option<HashMap<String, String>>> {
+        let id = self.run_command("ID NIL").await?;
+        let server_identification = parse_id(
+            &mut self.conn.stream,
+            self.unsolicited_responses_tx.clone(),
+            id,
+        )
+        .await?;
+        Ok(server_identification)
+    }
+
+    // these are only here because they are public interface, the rest is in `Connection`
+    /// Runs a command and checks if it returns OK.
+    pub async fn run_command_and_check_ok<S: AsRef<str>>(&mut self, command: S) -> Result<()> {
+        self.conn
+            .run_command_and_check_ok(
+                command.as_ref(),
+                Some(self.unsolicited_responses_tx.clone()),
+            )
+            .await?;
+
+        Ok(())
+    }
+
+    /// Runs any command passed to it.
+    pub async fn run_command<S: AsRef<str>>(&mut self, command: S) -> Result<RequestId> {
+        let id = self.conn.run_command(command.as_ref()).await?;
+
+        Ok(id)
+    }
+
+    /// Runs an arbitrary command, without adding a tag to it.
+    pub async fn run_command_untagged<S: AsRef<str>>(&mut self, command: S) -> Result<()> {
+        self.conn.run_command_untagged(command.as_ref()).await?;
+
+        Ok(())
+    }
+
+    /// Read the next response on the connection.
+    pub async fn read_response(&mut self) -> Option<io::Result<ResponseData>> {
+        self.conn.read_response().await
+    }
+}
+
+impl<T: Read + Write + Unpin + fmt::Debug> Connection<T> {
+    unsafe_pinned!(stream: ImapStream<T>);
+
+    /// Convert this connection into the raw underlying stream.
+    pub fn into_inner(self) -> T {
+        let Self { stream, .. } = self;
+        stream.into_inner()
+    }
+
+    /// Read the next response on the connection.
+    pub async fn read_response(&mut self) -> Option<io::Result<ResponseData>> {
+        self.stream.next().await
+    }
+
+    pub(crate) async fn run_command_untagged(&mut self, command: &str) -> Result<()> {
+        self.stream
+            .encode(Request(None, command.as_bytes().into()))
+            .await?;
+        self.stream.flush().await?;
+        Ok(())
+    }
+
+    pub(crate) async fn run_command(&mut self, command: &str) -> Result<RequestId> {
+        let request_id = self.request_ids.next().unwrap(); // safe: never returns Err
+        self.stream
+            .encode(Request(Some(request_id.clone()), command.as_bytes().into()))
+            .await?;
+        self.stream.flush().await?;
+        Ok(request_id)
+    }
+
+    /// Execute a command and check that the next response is a matching done.
+    pub async fn run_command_and_check_ok(
+        &mut self,
+        command: &str,
+        unsolicited: Option<channel::Sender<UnsolicitedResponse>>,
+    ) -> Result<()> {
+        let id = self.run_command(command).await?;
+        self.check_done_ok(&id, unsolicited).await?;
+
+        Ok(())
+    }
+
+    pub(crate) async fn check_done_ok(
+        &mut self,
+        id: &RequestId,
+        unsolicited: Option<channel::Sender<UnsolicitedResponse>>,
+    ) -> Result<()> {
+        if let Some(first_res) = self.stream.next().await {
+            self.check_done_ok_from(id, unsolicited, first_res?).await
+        } else {
+            Err(Error::ConnectionLost)
+        }
+    }
+
+    pub(crate) async fn check_done_ok_from(
+        &mut self,
+        id: &RequestId,
+        unsolicited: Option<channel::Sender<UnsolicitedResponse>>,
+        mut response: ResponseData,
+    ) -> Result<()> {
+        loop {
+            if let Response::Done {
+                status,
+                code,
+                information,
+                tag,
+            } = response.parsed()
+            {
+                self.check_status_ok(status, code.as_ref(), information.as_deref())?;
+
+                if tag == id {
+                    return Ok(());
+                }
+            }
+
+            if let Some(unsolicited) = unsolicited.clone() {
+                handle_unilateral(response, unsolicited).await;
+            }
+
+            if let Some(res) = self.stream.next().await {
+                response = res?;
+            } else {
+                return Err(Error::ConnectionLost);
+            }
+        }
+    }
+
+    pub(crate) fn check_status_ok(
+        &self,
+        status: &imap_proto::Status,
+        code: Option<&imap_proto::ResponseCode<'_>>,
+        information: Option<&str>,
+    ) -> Result<()> {
+        use imap_proto::Status;
+        match status {
+            Status::Ok => Ok(()),
+            Status::Bad => Err(Error::Bad(format!(
+                "code: {:?}, info: {:?}",
+                code, information
+            ))),
+            Status::No => Err(Error::No(format!(
+                "code: {:?}, info: {:?}",
+                code, information
+            ))),
+            _ => Err(Error::Io(io::Error::new(
+                io::ErrorKind::Other,
+                format!(
+                    "status: {:?}, code: {:?}, information: {:?}",
+                    status, code, information
+                ),
+            ))),
+        }
+    }
+}
+
+fn validate_str(value: &str) -> Result<String> {
+    let quoted = quote!(value);
+    if quoted.find('\n').is_some() {
+        return Err(Error::Validate(ValidateError('\n')));
+    }
+    if quoted.find('\r').is_some() {
+        return Err(Error::Validate(ValidateError('\r')));
+    }
+    Ok(quoted)
+}
+
+#[cfg(test)]
+mod tests {
+    use pretty_assertions::assert_eq;
+
+    use super::super::error::Result;
+    use super::super::mock_stream::MockStream;
+    use super::*;
+    use std::borrow::Cow;
+    use std::future::Future;
+
+    use async_std::sync::{Arc, Mutex};
+    use imap_proto::Status;
+
+    macro_rules! mock_client {
+        ($s:expr) => {
+            Client::new($s)
+        };
+    }
+
+    macro_rules! mock_session {
+        ($s:expr) => {
+            Session::new(mock_client!($s).conn)
+        };
+    }
+
+    macro_rules! assert_eq_bytes {
+        ($a:expr, $b:expr, $c:expr) => {
+            assert_eq!(
+                std::str::from_utf8($a).unwrap(),
+                std::str::from_utf8($b).unwrap(),
+                $c
+            )
+        };
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn fetch_body() {
+        let response = "a0 OK Logged in.\r\n\
+                        * 2 FETCH (BODY[TEXT] {3}\r\nfoo)\r\n\
+                        a0 OK FETCH completed\r\n";
+        let mut session = mock_session!(MockStream::new(response.as_bytes().to_vec()));
+        session.read_response().await.unwrap().unwrap();
+        session.read_response().await.unwrap().unwrap();
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn readline_delay_read() {
+        let greeting = "* OK Dovecot ready.\r\n";
+        let mock_stream = MockStream::default()
+            .with_buf(greeting.as_bytes().to_vec())
+            .with_delay();
+
+        let mut client = mock_client!(mock_stream);
+        let actual_response = client.read_response().await.unwrap().unwrap();
+        assert_eq!(
+            actual_response.parsed(),
+            &Response::Data {
+                status: Status::Ok,
+                code: None,
+                information: Some(Cow::Borrowed("Dovecot ready.")),
+            }
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn readline_eof() {
+        let mock_stream = MockStream::default().with_eof();
+        let mut client = mock_client!(mock_stream);
+        let res = client.read_response().await;
+        assert!(res.is_none());
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    #[should_panic]
+    async fn readline_err() {
+        // TODO Check the error test
+        let mock_stream = MockStream::default().with_err();
+        let mut client = mock_client!(mock_stream);
+        client.read_response().await.unwrap().unwrap();
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn authenticate() {
+        let response = b"+ YmFy\r\n\
+                         A0001 OK Logged in\r\n"
+            .to_vec();
+        let command = "A0001 AUTHENTICATE PLAIN\r\n\
+                       Zm9v\r\n";
+        let mock_stream = MockStream::new(response);
+        let client = mock_client!(mock_stream);
+        enum Authenticate {
+            Auth,
+        }
+        impl Authenticator for &Authenticate {
+            type Response = Vec<u8>;
+            fn process(&mut self, challenge: &[u8]) -> Self::Response {
+                assert!(challenge == b"bar", "Invalid authenticate challenge");
+                b"foo".to_vec()
+            }
+        }
+        let session = client
+            .authenticate("PLAIN", &Authenticate::Auth)
+            .await
+            .ok()
+            .unwrap();
+        assert_eq_bytes!(
+            &session.stream.inner.written_buf,
+            command.as_bytes(),
+            "Invalid authenticate command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn login() {
+        let response = b"A0001 OK Logged in\r\n".to_vec();
+        let username = "username";
+        let password = "password";
+        let command = format!("A0001 LOGIN {} {}\r\n", quote!(username), quote!(password));
+        let mock_stream = MockStream::new(response);
+        let client = mock_client!(mock_stream);
+        if let Ok(session) = client.login(username, password).await {
+            assert_eq!(
+                session.stream.inner.written_buf,
+                command.as_bytes().to_vec(),
+                "Invalid login command"
+            );
+        } else {
+            unreachable!("invalid login");
+        }
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn logout() {
+        let response = b"A0001 OK Logout completed.\r\n".to_vec();
+        let command = "A0001 LOGOUT\r\n";
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.logout().await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid logout command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn rename() {
+        let response = b"A0001 OK RENAME completed\r\n".to_vec();
+        let current_mailbox_name = "INBOX";
+        let new_mailbox_name = "NEWINBOX";
+        let command = format!(
+            "A0001 RENAME {} {}\r\n",
+            quote!(current_mailbox_name),
+            quote!(new_mailbox_name)
+        );
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session
+            .rename(current_mailbox_name, new_mailbox_name)
+            .await
+            .unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid rename command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn subscribe() {
+        let response = b"A0001 OK SUBSCRIBE completed\r\n".to_vec();
+        let mailbox = "INBOX";
+        let command = format!("A0001 SUBSCRIBE {}\r\n", quote!(mailbox));
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.subscribe(mailbox).await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid subscribe command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn unsubscribe() {
+        let response = b"A0001 OK UNSUBSCRIBE completed\r\n".to_vec();
+        let mailbox = "INBOX";
+        let command = format!("A0001 UNSUBSCRIBE {}\r\n", quote!(mailbox));
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.unsubscribe(mailbox).await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid unsubscribe command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn expunge() {
+        let response = b"A0001 OK EXPUNGE completed\r\n".to_vec();
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.expunge().await.unwrap().collect::<Vec<_>>().await;
+        assert!(
+            session.stream.inner.written_buf == b"A0001 EXPUNGE\r\n".to_vec(),
+            "Invalid expunge command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn uid_expunge() {
+        let response = b"* 2 EXPUNGE\r\n\
+            * 3 EXPUNGE\r\n\
+            * 4 EXPUNGE\r\n\
+            A0001 OK UID EXPUNGE completed\r\n"
+            .to_vec();
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session
+            .uid_expunge("2:4")
+            .await
+            .unwrap()
+            .collect::<Vec<_>>()
+            .await;
+        assert!(
+            session.stream.inner.written_buf == b"A0001 UID EXPUNGE 2:4\r\n".to_vec(),
+            "Invalid expunge command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn check() {
+        let response = b"A0001 OK CHECK completed\r\n".to_vec();
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.check().await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == b"A0001 CHECK\r\n".to_vec(),
+            "Invalid check command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn examine() {
+        let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\
+            * OK [PERMANENTFLAGS ()] Read-only mailbox.\r\n\
+            * 1 EXISTS\r\n\
+            * 1 RECENT\r\n\
+            * OK [UNSEEN 1] First unseen.\r\n\
+            * OK [UIDVALIDITY 1257842737] UIDs valid\r\n\
+            * OK [UIDNEXT 2] Predicted next UID\r\n\
+            A0001 OK [READ-ONLY] Select completed.\r\n"
+            .to_vec();
+        let expected_mailbox = Mailbox {
+            flags: vec![
+                Flag::Answered,
+                Flag::Flagged,
+                Flag::Deleted,
+                Flag::Seen,
+                Flag::Draft,
+            ],
+            exists: 1,
+            recent: 1,
+            unseen: Some(1),
+            permanent_flags: vec![],
+            uid_next: Some(2),
+            uid_validity: Some(1257842737),
+            highest_modseq: None,
+        };
+        let mailbox_name = "INBOX";
+        let command = format!("A0001 EXAMINE {}\r\n", quote!(mailbox_name));
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        let mailbox = session.examine(mailbox_name).await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid examine command"
+        );
+        assert_eq!(mailbox, expected_mailbox);
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn select() {
+        let response = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n\
+            * OK [PERMANENTFLAGS (\\* \\Answered \\Flagged \\Deleted \\Draft \\Seen)] \
+              Read-only mailbox.\r\n\
+            * 1 EXISTS\r\n\
+            * 1 RECENT\r\n\
+            * OK [UNSEEN 1] First unseen.\r\n\
+            * OK [UIDVALIDITY 1257842737] UIDs valid\r\n\
+            * OK [UIDNEXT 2] Predicted next UID\r\n\
+            * OK [HIGHESTMODSEQ 90060115205545359] Highest mailbox modsequence\r\n\
+            A0001 OK [READ-ONLY] Select completed.\r\n"
+            .to_vec();
+        let expected_mailbox = Mailbox {
+            flags: vec![
+                Flag::Answered,
+                Flag::Flagged,
+                Flag::Deleted,
+                Flag::Seen,
+                Flag::Draft,
+            ],
+            exists: 1,
+            recent: 1,
+            unseen: Some(1),
+            permanent_flags: vec![
+                Flag::MayCreate,
+                Flag::Answered,
+                Flag::Flagged,
+                Flag::Deleted,
+                Flag::Draft,
+                Flag::Seen,
+            ],
+            uid_next: Some(2),
+            uid_validity: Some(1257842737),
+            highest_modseq: Some(90060115205545359),
+        };
+        let mailbox_name = "INBOX";
+        let command = format!("A0001 SELECT {}\r\n", quote!(mailbox_name));
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        let mailbox = session.select(mailbox_name).await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid select command"
+        );
+        assert_eq!(mailbox, expected_mailbox);
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn search() {
+        let response = b"* SEARCH 1 2 3 4 5\r\n\
+            A0001 OK Search completed\r\n"
+            .to_vec();
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        let ids = session.search("Unseen").await.unwrap();
+        let ids: HashSet<u32> = ids.iter().cloned().collect();
+        assert!(
+            session.stream.inner.written_buf == b"A0001 SEARCH Unseen\r\n".to_vec(),
+            "Invalid search command"
+        );
+        assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect());
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn uid_search() {
+        let response = b"* SEARCH 1 2 3 4 5\r\n\
+            A0001 OK Search completed\r\n"
+            .to_vec();
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        let ids = session.uid_search("Unseen").await.unwrap();
+        let ids: HashSet<Uid> = ids.iter().cloned().collect();
+        assert!(
+            session.stream.inner.written_buf == b"A0001 UID SEARCH Unseen\r\n".to_vec(),
+            "Invalid search command"
+        );
+        assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect());
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn uid_search_unordered() {
+        let response = b"* SEARCH 1 2 3 4 5\r\n\
+            A0002 OK CAPABILITY completed\r\n\
+            A0001 OK Search completed\r\n"
+            .to_vec();
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        let ids = session.uid_search("Unseen").await.unwrap();
+        let ids: HashSet<Uid> = ids.iter().cloned().collect();
+        assert!(
+            session.stream.inner.written_buf == b"A0001 UID SEARCH Unseen\r\n".to_vec(),
+            "Invalid search command"
+        );
+        assert_eq!(ids, [1, 2, 3, 4, 5].iter().cloned().collect());
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn capability() {
+        let response = b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n\
+            A0001 OK CAPABILITY completed\r\n"
+            .to_vec();
+        let expected_capabilities = vec!["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"];
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        let capabilities = session.capabilities().await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == b"A0001 CAPABILITY\r\n".to_vec(),
+            "Invalid capability command"
+        );
+        assert_eq!(capabilities.len(), 4);
+        for e in expected_capabilities {
+            assert!(capabilities.has_str(e));
+        }
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn create() {
+        let response = b"A0001 OK CREATE completed\r\n".to_vec();
+        let mailbox_name = "INBOX";
+        let command = format!("A0001 CREATE {}\r\n", quote!(mailbox_name));
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.create(mailbox_name).await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid create command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn delete() {
+        let response = b"A0001 OK DELETE completed\r\n".to_vec();
+        let mailbox_name = "INBOX";
+        let command = format!("A0001 DELETE {}\r\n", quote!(mailbox_name));
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.delete(mailbox_name).await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid delete command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn noop() {
+        let response = b"A0001 OK NOOP completed\r\n".to_vec();
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.noop().await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == b"A0001 NOOP\r\n".to_vec(),
+            "Invalid noop command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn close() {
+        let response = b"A0001 OK CLOSE completed\r\n".to_vec();
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.close().await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == b"A0001 CLOSE\r\n".to_vec(),
+            "Invalid close command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn store() {
+        generic_store(" ", |c, set, query| async move {
+            c.lock()
+                .await
+                .store(set, query)
+                .await?
+                .collect::<Vec<_>>()
+                .await;
+            Ok(())
+        })
+        .await;
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn uid_store() {
+        generic_store(" UID ", |c, set, query| async move {
+            c.lock()
+                .await
+                .uid_store(set, query)
+                .await?
+                .collect::<Vec<_>>()
+                .await;
+            Ok(())
+        })
+        .await;
+    }
+
+    async fn generic_store<'a, F, T, K>(prefix: &'a str, op: F)
+    where
+        F: 'a + FnOnce(Arc<Mutex<Session<MockStream>>>, &'a str, &'a str) -> K,
+        K: 'a + Future<Output = Result<T>>,
+    {
+        let res = "* 2 FETCH (FLAGS (\\Deleted \\Seen))\r\n\
+                   * 3 FETCH (FLAGS (\\Deleted))\r\n\
+                   * 4 FETCH (FLAGS (\\Deleted \\Flagged \\Seen))\r\n\
+                   A0001 OK STORE completed\r\n";
+
+        generic_with_uid(res, "STORE", "2.4", "+FLAGS (\\Deleted)", prefix, op).await;
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn copy() {
+        generic_copy(" ", |c, set, query| async move {
+            c.lock().await.copy(set, query).await?;
+            Ok(())
+        })
+        .await;
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn uid_copy() {
+        generic_copy(" UID ", |c, set, query| async move {
+            c.lock().await.uid_copy(set, query).await?;
+            Ok(())
+        })
+        .await;
+    }
+
+    async fn generic_copy<'a, F, T, K>(prefix: &'a str, op: F)
+    where
+        F: 'a + FnOnce(Arc<Mutex<Session<MockStream>>>, &'a str, &'a str) -> K,
+        K: 'a + Future<Output = Result<T>>,
+    {
+        generic_with_uid(
+            "A0001 OK COPY completed\r\n",
+            "COPY",
+            "2:4",
+            "MEETING",
+            prefix,
+            op,
+        )
+        .await;
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn mv() {
+        let response = b"* OK [COPYUID 1511554416 142,399 41:42] Moved UIDs.\r\n\
+            * 2 EXPUNGE\r\n\
+            * 1 EXPUNGE\r\n\
+            A0001 OK Move completed\r\n"
+            .to_vec();
+        let mailbox_name = "MEETING";
+        let command = format!("A0001 MOVE 1:2 {}\r\n", quote!(mailbox_name));
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.mv("1:2", mailbox_name).await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid move command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn uid_mv() {
+        let response = b"* OK [COPYUID 1511554416 142,399 41:42] Moved UIDs.\r\n\
+            * 2 EXPUNGE\r\n\
+            * 1 EXPUNGE\r\n\
+            A0001 OK Move completed\r\n"
+            .to_vec();
+        let mailbox_name = "MEETING";
+        let command = format!("A0001 UID MOVE 41:42 {}\r\n", quote!(mailbox_name));
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+        session.uid_mv("41:42", mailbox_name).await.unwrap();
+        assert!(
+            session.stream.inner.written_buf == command.as_bytes().to_vec(),
+            "Invalid uid move command"
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn fetch() {
+        generic_fetch(" ", |c, seq, query| async move {
+            c.lock()
+                .await
+                .fetch(seq, query)
+                .await?
+                .collect::<Vec<_>>()
+                .await;
+
+            Ok(())
+        })
+        .await;
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn uid_fetch() {
+        generic_fetch(" UID ", |c, seq, query| async move {
+            c.lock()
+                .await
+                .uid_fetch(seq, query)
+                .await?
+                .collect::<Vec<_>>()
+                .await;
+            Ok(())
+        })
+        .await;
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn fetch_unexpected_eof() {
+        // Connection is lost, there will never be any response.
+        let response = b"".to_vec();
+
+        let mock_stream = MockStream::new(response);
+        let mut session = mock_session!(mock_stream);
+
+        {
+            let mut fetch_result = session
+                .uid_fetch("1:*", "(FLAGS BODY.PEEK[])")
+                .await
+                .unwrap();
+
+            // Unexpected EOF.
+            let err = fetch_result.next().await.unwrap().unwrap_err();
+            let Error::Io(io_err) = err else {
+                panic!("Unexpected error type: {err}")
+            };
+            assert_eq!(io_err.kind(), io::ErrorKind::UnexpectedEof);
+        }
+
+        assert_eq!(
+            session.stream.inner.written_buf,
+            b"A0001 UID FETCH 1:* (FLAGS BODY.PEEK[])\r\n".to_vec()
+        );
+    }
+
+    async fn generic_fetch<'a, F, T, K>(prefix: &'a str, op: F)
+    where
+        F: 'a + FnOnce(Arc<Mutex<Session<MockStream>>>, &'a str, &'a str) -> K,
+        K: 'a + Future<Output = Result<T>>,
+    {
+        generic_with_uid(
+            "A0001 OK FETCH completed\r\n",
+            "FETCH",
+            "1",
+            "BODY[]",
+            prefix,
+            op,
+        )
+        .await;
+    }
+
+    async fn generic_with_uid<'a, F, T, K>(
+        res: &'a str,
+        cmd: &'a str,
+        seq: &'a str,
+        query: &'a str,
+        prefix: &'a str,
+        op: F,
+    ) where
+        F: 'a + FnOnce(Arc<Mutex<Session<MockStream>>>, &'a str, &'a str) -> K,
+        K: 'a + Future<Output = Result<T>>,
+    {
+        let resp = res.as_bytes().to_vec();
+        let line = format!("A0001{}{} {} {}\r\n", prefix, cmd, seq, query);
+        let session = Arc::new(Mutex::new(mock_session!(MockStream::new(resp))));
+
+        {
+            let _ = op(session.clone(), seq, query).await.unwrap();
+        }
+        assert!(
+            session.lock().await.stream.inner.written_buf == line.as_bytes().to_vec(),
+            "Invalid command"
+        );
+    }
+
+    #[test]
+    fn quote_backslash() {
+        assert_eq!("\"test\\\\text\"", quote!(r"test\text"));
+    }
+
+    #[test]
+    fn quote_dquote() {
+        assert_eq!("\"test\\\"text\"", quote!("test\"text"));
+    }
+
+    #[test]
+    fn validate_random() {
+        assert_eq!(
+            "\"~iCQ_k;>[&\\\"sVCvUW`e<<P!wJ\"",
+            &validate_str("~iCQ_k;>[&\"sVCvUW`e<<P!wJ").unwrap()
+        );
+    }
+
+    #[test]
+    fn validate_newline() {
+        if let Err(ref e) = validate_str("test\nstring") {
+            if let Error::Validate(ref ve) = e {
+                if ve.0 == '\n' {
+                    return;
+                }
+            }
+            panic!("Wrong error: {:?}", e);
+        }
+        panic!("No error");
+    }
+
+    #[test]
+    #[allow(unreachable_patterns)]
+    fn validate_carriage_return() {
+        if let Err(ref e) = validate_str("test\rstring") {
+            if let Error::Validate(ref ve) = e {
+                if ve.0 == '\r' {
+                    return;
+                }
+            }
+            panic!("Wrong error: {:?}", e);
+        }
+        panic!("No error");
+    }
+
+    /// Emulates a server responding to `FETCH` requests
+    /// with a body of 76 bytes of headers and N 74-byte lines,
+    /// where N is the requested message sequence number.
+    #[cfg(feature = "runtime-tokio")]
+    async fn handle_client(stream: tokio::io::DuplexStream) -> Result<()> {
+        use tokio::io::AsyncBufReadExt;
+
+        let (reader, mut writer) = tokio::io::split(stream);
+        let reader = tokio::io::BufReader::new(reader);
+
+        let mut lines = reader.lines();
+        while let Some(line) = lines.next_line().await? {
+            let (request_id, request) = line.split_once(' ').unwrap();
+            eprintln!("Received request {request_id}.");
+
+            let (id, _) = request
+                .strip_prefix("FETCH ")
+                .unwrap()
+                .split_once(' ')
+                .unwrap();
+            let id = id.parse().unwrap();
+
+            let mut body = concat!(
+                "From: Bob <bob@example.com>\r\n",
+                "To: Alice <alice@example.org>\r\n",
+                "Subject: Test\r\n",
+                "Message-Id: <foobar@example.com>\r\n",
+                "Date: Sun, 22 Mar 2020 00:00:00 +0100\r\n",
+                "\r\n",
+            )
+            .to_string();
+            for _ in 1..id {
+                body +=
+                    "012345678901234567890123456789012345678901234567890123456789012345678901\r\n";
+            }
+            let body_len = body.len();
+
+            let response = format!("* {id} FETCH (RFC822.SIZE {body_len} BODY[] {{{body_len}}}\r\n{body} FLAGS (\\Seen))\r\n");
+            writer.write_all(response.as_bytes()).await?;
+            writer
+                .write_all(format!("{request_id} OK FETCH completed\r\n").as_bytes())
+                .await?;
+            writer.flush().await?;
+        }
+
+        Ok(())
+    }
+
+    /// Test requestng 1000 messages each larger than a previous one.
+    ///
+    /// This is a regression test for v0.6.0 async-imap,
+    /// which sometimes failed to allocate free buffer space,
+    /// read into a buffer of zero size and erroneously detected it
+    /// as the end of stream.
+    #[cfg(feature = "runtime-tokio")]
+    #[cfg_attr(
+        feature = "runtime-tokio",
+        tokio::test(flavor = "multi_thread", worker_threads = 2)
+    )]
+    async fn large_fetch() -> Result<()> {
+        use futures::TryStreamExt;
+
+        let (client, server) = tokio::io::duplex(4096);
+        tokio::spawn(handle_client(server));
+
+        let client = crate::Client::new(client);
+        let mut imap_session = Session::new(client.conn);
+
+        for i in 200..300 {
+            eprintln!("Fetching {i}.");
+            let mut messages_stream = imap_session
+                .fetch(format!("{i}"), "(RFC822.SIZE BODY.PEEK[] FLAGS)")
+                .await?;
+            let fetch = messages_stream
+                .try_next()
+                .await?
+                .expect("no FETCH returned");
+            let body = fetch.body().expect("message did not have a body!");
+            assert_eq!(body.len(), 76 + 74 * i);
+
+            let no_fetch = messages_stream.try_next().await?;
+            assert!(no_fetch.is_none());
+            drop(messages_stream);
+        }
+
+        Ok(())
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn status() {
+        {
+            let response = b"* STATUS INBOX (UIDNEXT 25)\r\n\
+                A0001 OK [CLIENTBUG] Status on selected mailbox completed (0.001 + 0.000 secs).\r\n"
+                .to_vec();
+
+            let mock_stream = MockStream::new(response);
+            let mut session = mock_session!(mock_stream);
+            let status = session.status("INBOX", "(UIDNEXT)").await.unwrap();
+            assert_eq!(
+                session.stream.inner.written_buf,
+                b"A0001 STATUS \"INBOX\" (UIDNEXT)\r\n".to_vec()
+            );
+            assert_eq!(status.uid_next, Some(25));
+        }
+
+        {
+            let response = b"* STATUS INBOX (RECENT 15)\r\n\
+                A0001 OK STATUS completed\r\n"
+                .to_vec();
+
+            let mock_stream = MockStream::new(response);
+            let mut session = mock_session!(mock_stream);
+            let status = session.status("INBOX", "(RECENT)").await.unwrap();
+            assert_eq!(
+                session.stream.inner.written_buf,
+                b"A0001 STATUS \"INBOX\" (RECENT)\r\n".to_vec()
+            );
+            assert_eq!(status.recent, 15);
+        }
+
+        {
+            // Example from RFC 3501.
+            let response = b"* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)\r\n\
+                A0001 OK STATUS completed\r\n"
+                .to_vec();
+
+            let mock_stream = MockStream::new(response);
+            let mut session = mock_session!(mock_stream);
+            let status = session
+                .status("blurdybloop", "(UIDNEXT MESSAGES)")
+                .await
+                .unwrap();
+            assert_eq!(
+                session.stream.inner.written_buf,
+                b"A0001 STATUS \"blurdybloop\" (UIDNEXT MESSAGES)\r\n".to_vec()
+            );
+            assert_eq!(status.uid_next, Some(44292));
+            assert_eq!(status.exists, 231);
+        }
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn get_metadata() {
+        {
+            let response = b"* METADATA \"INBOX\" (/private/comment \"My own comment\")\r\n\
+                A0001 OK GETMETADATA complete\r\n"
+                .to_vec();
+
+            let mock_stream = MockStream::new(response);
+            let mut session = mock_session!(mock_stream);
+            let metadata = session
+                .get_metadata("INBOX", "", "/private/comment")
+                .await
+                .unwrap();
+            assert_eq!(
+                session.stream.inner.written_buf,
+                b"A0001 GETMETADATA \"INBOX\" /private/comment\r\n".to_vec()
+            );
+            assert_eq!(metadata.len(), 1);
+            assert_eq!(metadata[0].entry, "/private/comment");
+            assert_eq!(metadata[0].value.as_ref().unwrap(), "My own comment");
+        }
+
+        {
+            let response = b"* METADATA \"INBOX\" (/shared/comment \"Shared comment\" /private/comment \"My own comment\")\r\n\
+                A0001 OK GETMETADATA complete\r\n"
+                .to_vec();
+
+            let mock_stream = MockStream::new(response);
+            let mut session = mock_session!(mock_stream);
+            let metadata = session
+                .get_metadata("INBOX", "", "(/shared/comment /private/comment)")
+                .await
+                .unwrap();
+            assert_eq!(
+                session.stream.inner.written_buf,
+                b"A0001 GETMETADATA \"INBOX\" (/shared/comment /private/comment)\r\n".to_vec()
+            );
+            assert_eq!(metadata.len(), 2);
+            assert_eq!(metadata[0].entry, "/shared/comment");
+            assert_eq!(metadata[0].value.as_ref().unwrap(), "Shared comment");
+            assert_eq!(metadata[1].entry, "/private/comment");
+            assert_eq!(metadata[1].value.as_ref().unwrap(), "My own comment");
+        }
+
+        {
+            let response = b"* METADATA \"\" (/shared/comment {15}\r\nChatmail server /shared/admin {28}\r\nmailto:root@nine.testrun.org)\r\n\
+                A0001 OK OK Getmetadata completed (0.001 + 0.000 secs).\r\n"
+                .to_vec();
+
+            let mock_stream = MockStream::new(response);
+            let mut session = mock_session!(mock_stream);
+            let metadata = session
+                .get_metadata("", "", "(/shared/comment /shared/admin)")
+                .await
+                .unwrap();
+            assert_eq!(
+                session.stream.inner.written_buf,
+                b"A0001 GETMETADATA \"\" (/shared/comment /shared/admin)\r\n".to_vec()
+            );
+            assert_eq!(metadata.len(), 2);
+            assert_eq!(metadata[0].entry, "/shared/comment");
+            assert_eq!(metadata[0].value.as_ref().unwrap(), "Chatmail server");
+            assert_eq!(metadata[1].entry, "/shared/admin");
+            assert_eq!(
+                metadata[1].value.as_ref().unwrap(),
+                "mailto:root@nine.testrun.org"
+            );
+        }
+
+        {
+            let response = b"* METADATA \"\" (/shared/comment \"Chatmail server\")\r\n\
+                * METADATA \"\" (/shared/admin \"mailto:root@nine.testrun.org\")\r\n\
+                A0001 OK OK Getmetadata completed (0.001 + 0.000 secs).\r\n"
+                .to_vec();
+
+            let mock_stream = MockStream::new(response);
+            let mut session = mock_session!(mock_stream);
+            let metadata = session
+                .get_metadata("", "", "(/shared/comment /shared/admin)")
+                .await
+                .unwrap();
+            assert_eq!(
+                session.stream.inner.written_buf,
+                b"A0001 GETMETADATA \"\" (/shared/comment /shared/admin)\r\n".to_vec()
+            );
+            assert_eq!(metadata.len(), 2);
+            assert_eq!(metadata[0].entry, "/shared/comment");
+            assert_eq!(metadata[0].value.as_ref().unwrap(), "Chatmail server");
+            assert_eq!(metadata[1].entry, "/shared/admin");
+            assert_eq!(
+                metadata[1].value.as_ref().unwrap(),
+                "mailto:root@nine.testrun.org"
+            );
+        }
+
+        {
+            let response = b"* METADATA \"\" (/shared/comment NIL /shared/admin NIL)\r\n\
+                A0001 OK OK Getmetadata completed (0.001 + 0.000 secs).\r\n"
+                .to_vec();
+
+            let mock_stream = MockStream::new(response);
+            let mut session = mock_session!(mock_stream);
+            let metadata = session
+                .get_metadata("", "", "(/shared/comment /shared/admin)")
+                .await
+                .unwrap();
+            assert_eq!(
+                session.stream.inner.written_buf,
+                b"A0001 GETMETADATA \"\" (/shared/comment /shared/admin)\r\n".to_vec()
+            );
+            assert_eq!(metadata.len(), 2);
+            assert_eq!(metadata[0].entry, "/shared/comment");
+            assert_eq!(metadata[0].value, None);
+            assert_eq!(metadata[1].entry, "/shared/admin");
+            assert_eq!(metadata[1].value, None);
+        }
+    }
+}

+ 75 - 0
async-imap-wasi/src/error.rs

@@ -0,0 +1,75 @@
+//! IMAP error types.
+
+use std::io::Error as IoError;
+use std::str::Utf8Error;
+
+use base64::DecodeError;
+
+/// A convenience wrapper around `Result` for `imap::Error`.
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// A set of errors that can occur in the IMAP client
+#[derive(thiserror::Error, Debug)]
+#[non_exhaustive]
+pub enum Error {
+    /// An `io::Error` that occurred while trying to read or write to a network stream.
+    #[error("io: {0}")]
+    Io(#[from] IoError),
+    /// A BAD response from the IMAP server.
+    #[error("bad response: {0}")]
+    Bad(String),
+    /// A NO response from the IMAP server.
+    #[error("no response: {0}")]
+    No(String),
+    /// The connection was terminated unexpectedly.
+    #[error("connection lost")]
+    ConnectionLost,
+    /// Error parsing a server response.
+    #[error("parse: {0}")]
+    Parse(#[from] ParseError),
+    /// Command inputs were not valid [IMAP
+    /// strings](https://tools.ietf.org/html/rfc3501#section-4.3).
+    #[error("validate: {0}")]
+    Validate(#[from] ValidateError),
+    /// Error appending an e-mail.
+    #[error("could not append mail to mailbox")]
+    Append,
+}
+
+/// An error occured while trying to parse a server response.
+#[derive(thiserror::Error, Debug)]
+pub enum ParseError {
+    /// Indicates an error parsing the status response. Such as OK, NO, and BAD.
+    #[error("unable to parse status response")]
+    Invalid(Vec<u8>),
+    /// An unexpected response was encountered.
+    #[error("encountered unexpected parsed response: {0}")]
+    Unexpected(String),
+    /// The client could not find or decode the server's authentication challenge.
+    #[error("unable to parse authentication response: {0} - {1:?}")]
+    Authentication(String, Option<DecodeError>),
+    /// The client received data that was not UTF-8 encoded.
+    #[error("unable to parse data ({0:?}) as UTF-8 text: {1:?}")]
+    DataNotUtf8(Vec<u8>, #[source] Utf8Error),
+    /// The expected response for X was not found
+    #[error("expected response not found for: {0}")]
+    ExpectedResponseNotFound(String),
+}
+
+/// An [invalid character](https://tools.ietf.org/html/rfc3501#section-4.3) was found in an input
+/// string.
+#[derive(thiserror::Error, Debug)]
+#[error("invalid character in input: '{0}'")]
+pub struct ValidateError(pub char);
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn is_send<T: Send>(_t: T) {}
+
+    #[test]
+    fn test_send() {
+        is_send::<Result<usize>>(Ok(3));
+    }
+}

+ 93 - 0
async-imap-wasi/src/extensions/id.rs

@@ -0,0 +1,93 @@
+//! IMAP ID extension specified in [RFC2971](https://datatracker.ietf.org/doc/html/rfc2971)
+
+use async_channel as channel;
+use futures::io;
+use futures::prelude::*;
+use imap_proto::{self, RequestId, Response};
+use std::collections::HashMap;
+
+use crate::types::ResponseData;
+use crate::types::*;
+use crate::{
+    error::Result,
+    parse::{filter, handle_unilateral},
+};
+
+fn escape(s: &str) -> String {
+    s.replace('\\', r"\\").replace('\"', "\\\"")
+}
+
+/// Formats list of key-value pairs for ID command.
+///
+/// Returned list is not wrapped in parenthesis, the caller should do it.
+pub(crate) fn format_identification<'a, 'b>(
+    id: impl IntoIterator<Item = (&'a str, Option<&'b str>)>,
+) -> String {
+    id.into_iter()
+        .map(|(k, v)| {
+            format!(
+                "\"{}\" {}",
+                escape(k),
+                v.map_or("NIL".to_string(), |v| format!("\"{}\"", escape(v)))
+            )
+        })
+        .collect::<Vec<String>>()
+        .join(" ")
+}
+
+pub(crate) async fn parse_id<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> Result<Option<HashMap<String, String>>> {
+    let mut id = None;
+    while let Some(resp) = stream
+        .take_while(|res| filter(res, &command_tag))
+        .next()
+        .await
+    {
+        let resp = resp?;
+        match resp.parsed() {
+            Response::Id(res) => {
+                id = res.as_ref().map(|m| {
+                    m.iter()
+                        .map(|(k, v)| (k.to_string(), v.to_string()))
+                        .collect()
+                })
+            }
+            _ => {
+                handle_unilateral(resp, unsolicited.clone()).await;
+            }
+        }
+    }
+
+    Ok(id)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_format_identification() {
+        assert_eq!(
+            format_identification([("name", Some("MyClient"))]),
+            r#""name" "MyClient""#
+        );
+
+        assert_eq!(
+            format_identification([("name", Some(r#""MyClient"\"#))]),
+            r#""name" "\"MyClient\"\\""#
+        );
+
+        assert_eq!(
+            format_identification([("name", Some("MyClient")), ("version", Some("2.0"))]),
+            r#""name" "MyClient" "version" "2.0""#
+        );
+
+        assert_eq!(
+            format_identification([("name", None), ("version", Some("2.0"))]),
+            r#""name" NIL "version" "2.0""#
+        );
+    }
+}

+ 232 - 0
async-imap-wasi/src/extensions/idle.rs

@@ -0,0 +1,232 @@
+//! Adds support for the IMAP IDLE command specificed in [RFC2177](https://tools.ietf.org/html/rfc2177).
+
+use std::fmt;
+use std::pin::Pin;
+use std::time::Duration;
+
+#[cfg(feature = "runtime-async-std")]
+use async_std::{
+    future::timeout,
+    io::{Read, Write},
+};
+use futures::prelude::*;
+use futures::task::{Context, Poll};
+use imap_proto::{RequestId, Response, Status};
+use stop_token::prelude::*;
+#[cfg(feature = "runtime-tokio")]
+use tokio::{
+    io::{AsyncRead as Read, AsyncWrite as Write},
+    time::timeout,
+};
+
+use crate::client::Session;
+use crate::error::Result;
+use crate::parse::handle_unilateral;
+use crate::types::ResponseData;
+
+/// `Handle` allows a client to block waiting for changes to the remote mailbox.
+///
+/// The handle blocks using the [`IDLE` command](https://tools.ietf.org/html/rfc2177#section-3)
+/// specificed in [RFC 2177](https://tools.ietf.org/html/rfc2177) until the underlying server state
+/// changes in some way. While idling does inform the client what changes happened on the server,
+/// this implementation will currently just block until _anything_ changes, and then notify the
+///
+/// Note that the server MAY consider a client inactive if it has an IDLE command running, and if
+/// such a server has an inactivity timeout it MAY log the client off implicitly at the end of its
+/// timeout period.  Because of that, clients using IDLE are advised to terminate the IDLE and
+/// re-issue it at least every 29 minutes to avoid being logged off. [`Handle::wait`]
+/// does this. This still allows a client to receive immediate mailbox updates even though it need
+/// only "poll" at half hour intervals.
+///
+/// As long as a [`Handle`] is active, the mailbox cannot be otherwise accessed.
+#[derive(Debug)]
+pub struct Handle<T: Read + Write + Unpin + fmt::Debug> {
+    session: Session<T>,
+    id: Option<RequestId>,
+}
+
+impl<T: Read + Write + Unpin + fmt::Debug> Unpin for Handle<T> {}
+
+impl<T: Read + Write + Unpin + fmt::Debug + Send> Stream for Handle<T> {
+    type Item = std::io::Result<ResponseData>;
+
+    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+        self.as_mut().session().get_stream().poll_next(cx)
+    }
+}
+
+/// A stream of server responses after sending `IDLE`.
+#[derive(Debug)]
+#[must_use = "futures do nothing unless polled"]
+pub struct IdleStream<'a, St> {
+    stream: &'a mut St,
+}
+
+impl<St: Unpin> Unpin for IdleStream<'_, St> {}
+
+impl<'a, St: Stream + Unpin> IdleStream<'a, St> {
+    unsafe_pinned!(stream: &'a mut St);
+
+    pub(crate) fn new(stream: &'a mut St) -> Self {
+        IdleStream { stream }
+    }
+}
+
+impl<St: futures::stream::FusedStream + Unpin> futures::stream::FusedStream for IdleStream<'_, St> {
+    fn is_terminated(&self) -> bool {
+        self.stream.is_terminated()
+    }
+}
+
+impl<St: Stream + Unpin> Stream for IdleStream<'_, St> {
+    type Item = St::Item;
+
+    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+        self.stream().poll_next(cx)
+    }
+}
+
+/// Possible responses that happen on an open idle connection.
+#[derive(Debug, PartialEq, Eq)]
+pub enum IdleResponse {
+    /// The manual interrupt was used to interrupt the idle connection..
+    ManualInterrupt,
+    /// The idle connection timed out, because of the user set timeout.
+    Timeout,
+    /// The server has indicated that some new action has happened.
+    NewData(ResponseData),
+}
+
+// Make it possible to access the inner connection and modify its settings, such as read/write
+// timeouts.
+impl<T: Read + Write + Unpin + fmt::Debug> AsMut<T> for Handle<T> {
+    fn as_mut(&mut self) -> &mut T {
+        self.session.conn.stream.as_mut()
+    }
+}
+
+impl<T: Read + Write + Unpin + fmt::Debug + Send> Handle<T> {
+    unsafe_pinned!(session: Session<T>);
+
+    pub(crate) fn new(session: Session<T>) -> Handle<T> {
+        Handle { session, id: None }
+    }
+
+    /// Start listening to the server side responses.
+    /// Must be called after [`Handle::init`].
+    pub fn wait(
+        &mut self,
+    ) -> (
+        impl Future<Output = Result<IdleResponse>> + '_,
+        stop_token::StopSource,
+    ) {
+        self.wait_with_timeout(Duration::from_secs(24 * 60 * 60))
+    }
+
+    /// Start listening to the server side responses.
+    ///
+    /// Stops after the passed in `timeout` without any response from the server.
+    /// Timeout is reset by any response, including `* OK Still here` keepalives.
+    ///
+    /// Must be called after [Handle::init].
+    pub fn wait_with_timeout(
+        &mut self,
+        dur: Duration,
+    ) -> (
+        impl Future<Output = Result<IdleResponse>> + '_,
+        stop_token::StopSource,
+    ) {
+        assert!(
+            self.id.is_some(),
+            "Cannot listen to response without starting IDLE"
+        );
+
+        let sender = self.session.unsolicited_responses_tx.clone();
+
+        let interrupt = stop_token::StopSource::new();
+        let raw_stream = IdleStream::new(self);
+        let mut interruptible_stream = raw_stream.timeout_at(interrupt.token());
+
+        let fut = async move {
+            loop {
+                let Ok(res) = timeout(dur, interruptible_stream.next()).await else {
+                    return Ok(IdleResponse::Timeout);
+                };
+
+                let Some(Ok(resp)) = res else {
+                    return Ok(IdleResponse::ManualInterrupt);
+                };
+
+                let resp = resp?;
+                match resp.parsed() {
+                    Response::Data {
+                        status: Status::Ok, ..
+                    } => {
+                        // all good continue
+                    }
+                    Response::Continue { .. } => {
+                        // continuation, wait for it
+                    }
+                    Response::Done { .. } => {
+                        handle_unilateral(resp, sender.clone()).await;
+                    }
+                    _ => return Ok(IdleResponse::NewData(resp)),
+                }
+            }
+        };
+
+        (fut, interrupt)
+    }
+
+    /// Initialise the idle connection by sending the `IDLE` command to the server.
+    pub async fn init(&mut self) -> Result<()> {
+        let id = self.session.run_command("IDLE").await?;
+        self.id = Some(id);
+        while let Some(res) = self.session.stream.next().await {
+            let res = res?;
+            match res.parsed() {
+                Response::Continue { .. } => {
+                    return Ok(());
+                }
+                Response::Done {
+                    tag,
+                    status,
+                    information,
+                    ..
+                } => {
+                    if tag == self.id.as_ref().unwrap() {
+                        if let Status::Bad = status {
+                            return Err(std::io::Error::new(
+                                std::io::ErrorKind::ConnectionRefused,
+                                information.as_ref().unwrap().to_string(),
+                            )
+                            .into());
+                        }
+                    }
+                    handle_unilateral(res, self.session.unsolicited_responses_tx.clone()).await;
+                }
+                _ => {
+                    handle_unilateral(res, self.session.unsolicited_responses_tx.clone()).await;
+                }
+            }
+        }
+
+        Err(std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "").into())
+    }
+
+    /// Signal that we want to exit the idle connection, by sending the `DONE`
+    /// command to the server.
+    pub async fn done(mut self) -> Result<Session<T>> {
+        assert!(
+            self.id.is_some(),
+            "Cannot call DONE on a non initialized idle connection"
+        );
+        self.session.run_command_untagged("DONE").await?;
+        let sender = self.session.unsolicited_responses_tx.clone();
+        self.session
+            .check_done_ok(&self.id.expect("invalid setup"), Some(sender))
+            .await?;
+
+        Ok(self.session)
+    }
+}

+ 6 - 0
async-imap-wasi/src/extensions/mod.rs

@@ -0,0 +1,6 @@
+//! Implementations of various IMAP extensions.
+pub mod idle;
+
+pub mod quota;
+
+pub mod id;

+ 74 - 0
async-imap-wasi/src/extensions/quota.rs

@@ -0,0 +1,74 @@
+//! Adds support for the GETQUOTA and GETQUOTAROOT commands specificed in [RFC2087](https://tools.ietf.org/html/rfc2087).
+
+use async_channel as channel;
+use futures::io;
+use futures::prelude::*;
+use imap_proto::{self, RequestId, Response};
+
+use crate::types::*;
+use crate::{
+    error::Result,
+    parse::{filter, handle_unilateral},
+};
+use crate::{
+    error::{Error, ParseError},
+    types::{Quota, QuotaRoot, ResponseData},
+};
+
+pub(crate) async fn parse_get_quota<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> Result<Quota> {
+    let mut quota = None;
+    while let Some(resp) = stream
+        .take_while(|res| filter(res, &command_tag))
+        .next()
+        .await
+    {
+        let resp = resp?;
+        match resp.parsed() {
+            Response::Quota(q) => quota = Some(q.clone().into()),
+            _ => {
+                handle_unilateral(resp, unsolicited.clone()).await;
+            }
+        }
+    }
+
+    match quota {
+        Some(q) => Ok(q),
+        None => Err(Error::Parse(ParseError::ExpectedResponseNotFound(
+            "Quota, no quota response found".to_string(),
+        ))),
+    }
+}
+
+pub(crate) async fn parse_get_quota_root<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> Result<(Vec<QuotaRoot>, Vec<Quota>)> {
+    let mut roots: Vec<QuotaRoot> = Vec::new();
+    let mut quotas: Vec<Quota> = Vec::new();
+
+    while let Some(resp) = stream
+        .take_while(|res| filter(res, &command_tag))
+        .next()
+        .await
+    {
+        let resp = resp?;
+        match resp.parsed() {
+            Response::QuotaRoot(qr) => {
+                roots.push(qr.clone().into());
+            }
+            Response::Quota(q) => {
+                quotas.push(q.clone().into());
+            }
+            _ => {
+                handle_unilateral(resp, unsolicited.clone()).await;
+            }
+        }
+    }
+
+    Ok((roots, quotas))
+}

+ 468 - 0
async-imap-wasi/src/imap_stream.rs

@@ -0,0 +1,468 @@
+use std::fmt;
+use std::pin::Pin;
+
+#[cfg(feature = "runtime-async-std")]
+use async_std::io::{Read, Write, WriteExt};
+use bytes::BytesMut;
+use futures::stream::Stream;
+use futures::task::{Context, Poll};
+use futures::{io, ready};
+use nom::Needed;
+#[cfg(feature = "runtime-tokio")]
+use tokio::io::{AsyncRead as Read, AsyncWrite as Write, AsyncWriteExt};
+
+use crate::types::{Request, ResponseData};
+
+/// Wraps a stream, and parses incoming data as imap server messages. Writes outgoing data
+/// as imap client messages.
+#[derive(Debug)]
+pub struct ImapStream<R: Read + Write> {
+    // TODO: write some buffering logic
+    /// The underlying stream
+    pub(crate) inner: R,
+    /// Number of bytes the next decode operation needs if known.
+    /// If the buffer contains less than this, it is a waste of time to try to parse it.
+    /// If unknown, set it to 0, so decoding is always attempted.
+    decode_needs: usize,
+    /// The buffer.
+    buffer: Buffer,
+}
+
+impl<R: Read + Write + Unpin> ImapStream<R> {
+    /// Creates a new `ImapStream` based on the given `Read`er.
+    pub fn new(inner: R) -> Self {
+        ImapStream {
+            inner,
+            buffer: Buffer::new(),
+            decode_needs: 0,
+        }
+    }
+
+    pub async fn encode(&mut self, msg: Request) -> Result<(), io::Error> {
+        log::trace!(
+            "encode: input: {:?}, {:?}",
+            msg.0,
+            std::str::from_utf8(&msg.1)
+        );
+
+        if let Some(tag) = msg.0 {
+            self.inner.write_all(tag.as_bytes()).await?;
+            self.inner.write(b" ").await?;
+        }
+        self.inner.write_all(&msg.1).await?;
+        self.inner.write_all(b"\r\n").await?;
+
+        Ok(())
+    }
+
+    pub fn into_inner(self) -> R {
+        self.inner
+    }
+
+    /// Flushes the underlying stream.
+    pub async fn flush(&mut self) -> Result<(), io::Error> {
+        self.inner.flush().await
+    }
+
+    pub fn as_mut(&mut self) -> &mut R {
+        &mut self.inner
+    }
+
+    /// Attempts to decode a single response from the buffer.
+    ///
+    /// Returns `None` if the buffer does not contain enough data.
+    fn decode(&mut self) -> io::Result<Option<ResponseData>> {
+        if self.buffer.used() < self.decode_needs {
+            // We know that there is not enough data to decode anything
+            // from previous attempts.
+            return Ok(None);
+        }
+
+        let block = self.buffer.take_block();
+        // Be aware, now self.buffer is invalid until block is returned or reset!
+
+        let res = ResponseData::try_new_or_recover(block, |buf| {
+            let buf = &buf[..self.buffer.used()];
+            log::trace!("decode: input: {:?}", std::str::from_utf8(buf));
+            match imap_proto::parser::parse_response(buf) {
+                Ok((remaining, response)) => {
+                    // TODO: figure out if we can use a minimum required size for a response.
+                    self.decode_needs = 0;
+                    self.buffer.reset_with_data(remaining);
+                    Ok(response)
+                }
+                Err(nom::Err::Incomplete(Needed::Size(min))) => {
+                    log::trace!("decode: incomplete data, need minimum {} bytes", min);
+                    self.decode_needs = self.buffer.used() + usize::from(min);
+                    Err(None)
+                }
+                Err(nom::Err::Incomplete(_)) => {
+                    log::trace!("decode: incomplete data, need unknown number of bytes");
+                    self.decode_needs = 0;
+                    Err(None)
+                }
+                Err(other) => {
+                    self.decode_needs = 0;
+                    Err(Some(io::Error::new(
+                        io::ErrorKind::Other,
+                        format!("{:?} during parsing of {:?}", other, buf),
+                    )))
+                }
+            }
+        });
+        match res {
+            Ok(response) => Ok(Some(response)),
+            Err((heads, err)) => {
+                self.buffer.return_block(heads);
+                match err {
+                    Some(err) => Err(err),
+                    None => Ok(None),
+                }
+            }
+        }
+    }
+}
+
+/// Abstraction around needed buffer management.
+struct Buffer {
+    /// The buffer itself.
+    block: BytesMut,
+    /// Offset where used bytes range ends.
+    offset: usize,
+}
+
+impl Buffer {
+    const BLOCK_SIZE: usize = 1024 * 4;
+    const MAX_CAPACITY: usize = 512 * 1024 * 1024; // 512 MiB
+
+    fn new() -> Self {
+        Self {
+            block: BytesMut::zeroed(Self::BLOCK_SIZE),
+            offset: 0,
+        }
+    }
+
+    /// Returns the number of bytes in the buffer containing data.
+    fn used(&self) -> usize {
+        self.offset
+    }
+
+    /// Returns the unused part of the buffer to which new data can be written.
+    fn free_as_mut_slice(&mut self) -> &mut [u8] {
+        &mut self.block[self.offset..]
+    }
+
+    /// Indicate how many new bytes were written into the buffer.
+    ///
+    /// When new bytes are written into the slice returned by [`free_as_mut_slice`] this method
+    /// should be called to extend the used portion of the buffer to include the new data.
+    ///
+    /// You can not write past the end of the buffer, so extending more then there is free
+    /// space marks the entire buffer as used.
+    ///
+    /// [`free_as_mut_slice`]: Self::free_as_mut_slice
+    // aka advance()?
+    fn extend_used(&mut self, num_bytes: usize) {
+        self.offset += num_bytes;
+        if self.offset > self.block.len() {
+            self.offset = self.block.len();
+        }
+    }
+
+    /// Ensure the buffer has free capacity, optionally ensuring minimum buffer size.
+    fn ensure_capacity(&mut self, required: usize) -> io::Result<()> {
+        let free_bytes: usize = self.block.len() - self.offset;
+        let min_required_bytes: usize = required;
+        let extra_bytes_needed: usize = min_required_bytes.saturating_sub(self.block.len());
+        if free_bytes == 0 || extra_bytes_needed > 0 {
+            let increase = std::cmp::max(Buffer::BLOCK_SIZE, extra_bytes_needed);
+            self.grow(increase)?;
+        }
+
+        // Assert that the buffer at least one free byte.
+        debug_assert!(self.offset < self.block.len());
+        Ok(())
+    }
+
+    /// Grows the buffer, ensuring there are free bytes in the tail.
+    ///
+    /// The specified number of bytes is only a minimum.  The buffer could grow by more as
+    /// it will always grow in multiples of [`BLOCK_SIZE`].
+    ///
+    /// If the size would be larger than [`MAX_CAPACITY`] an error is returned.
+    ///
+    /// [`BLOCK_SIZE`]: Self::BLOCK_SIZE
+    /// [`MAX_CAPACITY`]: Self::MAX_CAPACITY
+    fn grow(&mut self, num_bytes: usize) -> io::Result<()> {
+        let min_size = self.block.len() + num_bytes;
+        let new_size = match min_size % Self::BLOCK_SIZE {
+            0 => min_size,
+            n => min_size + (Self::BLOCK_SIZE - n),
+        };
+        if new_size > Self::MAX_CAPACITY {
+            Err(io::Error::new(
+                io::ErrorKind::Other,
+                "incoming data too large",
+            ))
+        } else {
+            self.block.resize(new_size, 0);
+            Ok(())
+        }
+    }
+
+    /// Return the block backing the buffer.
+    ///
+    /// Next you *must* either return this block using [`return_block`] or call
+    /// [`reset_with_data`].
+    ///
+    /// [`return_block`]: Self::return_block
+    /// [`reset_with_data`]: Self::reset_with_data
+    // TODO: Enforce this with typestate.
+    fn take_block(&mut self) -> BytesMut {
+        std::mem::replace(&mut self.block, BytesMut::zeroed(Self::BLOCK_SIZE))
+    }
+
+    /// Reset the buffer to be a new allocation with given data copied in.
+    ///
+    /// This allows the previously returned block from `get_block` to be used in and owned
+    /// by the [ResponseData].
+    ///
+    /// This does not do any bounds checking to see if the new buffer would exceed the
+    /// maximum size.  It will however ensure that there is at least some free space at the
+    /// end of the buffer so that the next reading operation won't need to realloc right
+    /// away.  This could be wasteful if the next action on the buffer is another decode
+    /// rather than a read, but we don't know.
+    fn reset_with_data(&mut self, data: &[u8]) {
+        let min_size = data.len();
+        let new_size = match min_size % Self::BLOCK_SIZE {
+            0 => min_size + Self::BLOCK_SIZE,
+            n => min_size + (Self::BLOCK_SIZE - n),
+        };
+        self.block = BytesMut::zeroed(new_size);
+        self.block[..data.len()].copy_from_slice(data);
+
+        self.offset = data.len();
+    }
+
+    /// Return the block which backs this buffer.
+    fn return_block(&mut self, block: BytesMut) {
+        self.block = block;
+    }
+}
+
+impl fmt::Debug for Buffer {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("Buffer")
+            .field("used", &self.used())
+            .field("capacity", &self.block.capacity())
+            .finish()
+    }
+}
+
+impl<R: Read + Write + Unpin> Stream for ImapStream<R> {
+    type Item = io::Result<ResponseData>;
+
+    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+        let this = &mut *self;
+        if let Some(response) = this.decode()? {
+            return Poll::Ready(Some(Ok(response)));
+        }
+        loop {
+            this.buffer.ensure_capacity(this.decode_needs)?;
+            let buf = this.buffer.free_as_mut_slice();
+
+            // The buffer should have at least one byte free
+            // before we try reading into it
+            // so we can treat 0 bytes read as EOF.
+            // This is guaranteed by `ensure_capacity()` above
+            // even if it is called with 0 as an argument.
+            debug_assert!(!buf.is_empty());
+
+            #[cfg(feature = "runtime-async-std")]
+            let num_bytes_read = ready!(Pin::new(&mut this.inner).poll_read(cx, buf))?;
+
+            #[cfg(feature = "runtime-tokio")]
+            let num_bytes_read = {
+                let buf = &mut tokio::io::ReadBuf::new(buf);
+                let start = buf.filled().len();
+                ready!(Pin::new(&mut this.inner).poll_read(cx, buf))?;
+                buf.filled().len() - start
+            };
+
+            if num_bytes_read == 0 {
+                if this.buffer.used() > 0 {
+                    return Poll::Ready(Some(Err(io::Error::new(
+                        io::ErrorKind::UnexpectedEof,
+                        "bytes remaining in stream",
+                    ))));
+                }
+                return Poll::Ready(None);
+            }
+            this.buffer.extend_used(num_bytes_read);
+            if let Some(response) = this.decode()? {
+                return Poll::Ready(Some(Ok(response)));
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use std::io::Write;
+
+    #[test]
+    fn test_buffer_empty() {
+        let buf = Buffer::new();
+        assert_eq!(buf.used(), 0);
+
+        let mut buf = Buffer::new();
+        let slice: &[u8] = buf.free_as_mut_slice();
+        assert_eq!(slice.len(), Buffer::BLOCK_SIZE);
+        assert_eq!(slice.len(), buf.block.len());
+    }
+
+    #[test]
+    fn test_buffer_extend_use() {
+        let mut buf = Buffer::new();
+        buf.extend_used(3);
+        assert_eq!(buf.used(), 3);
+        let slice = buf.free_as_mut_slice();
+        assert_eq!(slice.len(), Buffer::BLOCK_SIZE - 3);
+
+        // Extend past the end of the buffer.
+        buf.extend_used(Buffer::BLOCK_SIZE);
+        assert_eq!(buf.used(), Buffer::BLOCK_SIZE);
+        assert_eq!(buf.offset, Buffer::BLOCK_SIZE);
+        assert_eq!(buf.block.len(), buf.offset);
+        let slice = buf.free_as_mut_slice();
+        assert_eq!(slice.len(), 0);
+    }
+
+    #[test]
+    fn test_buffer_write_read() {
+        let mut buf = Buffer::new();
+        let mut slice = buf.free_as_mut_slice();
+        slice.write_all(b"hello").unwrap();
+        buf.extend_used(b"hello".len());
+
+        let slice = &buf.block[..buf.used()];
+        assert_eq!(slice, b"hello");
+        assert_eq!(buf.free_as_mut_slice().len(), buf.block.len() - buf.offset);
+    }
+
+    #[test]
+    fn test_buffer_grow() {
+        let mut buf = Buffer::new();
+        assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE);
+        buf.grow(1).unwrap();
+        assert_eq!(buf.block.len(), 2 * Buffer::BLOCK_SIZE);
+
+        buf.grow(Buffer::BLOCK_SIZE + 1).unwrap();
+        assert_eq!(buf.block.len(), 4 * Buffer::BLOCK_SIZE);
+
+        let ret = buf.grow(Buffer::MAX_CAPACITY);
+        assert!(ret.is_err());
+    }
+
+    #[test]
+    fn test_buffer_ensure_capacity() {
+        // Initial state: 1 byte capacity left, initial size.
+        let mut buf = Buffer::new();
+        buf.extend_used(Buffer::BLOCK_SIZE - 1);
+        assert_eq!(buf.free_as_mut_slice().len(), 1);
+        assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE);
+
+        // Still has capacity, no size request.
+        buf.ensure_capacity(0).unwrap();
+        assert_eq!(buf.free_as_mut_slice().len(), 1);
+        assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE);
+
+        // No more capacity, initial size.
+        buf.extend_used(1);
+        assert_eq!(buf.free_as_mut_slice().len(), 0);
+        assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE);
+
+        // No capacity, no size request.
+        buf.ensure_capacity(0).unwrap();
+        assert_eq!(buf.free_as_mut_slice().len(), Buffer::BLOCK_SIZE);
+        assert_eq!(buf.block.len(), 2 * Buffer::BLOCK_SIZE);
+
+        // Some capacity, size request.
+        buf.extend_used(5);
+        assert_eq!(buf.offset, Buffer::BLOCK_SIZE + 5);
+        buf.ensure_capacity(3 * Buffer::BLOCK_SIZE - 6).unwrap();
+        assert_eq!(buf.free_as_mut_slice().len(), 2 * Buffer::BLOCK_SIZE - 5);
+        assert_eq!(buf.block.len(), 3 * Buffer::BLOCK_SIZE);
+    }
+
+    /// Regression test for a bug in ensure_capacity() caused
+    /// by a bug in byte-pool crate 0.2.2 dependency.
+    ///
+    /// ensure_capacity() sometimes did not ensure that
+    /// at least one byte is available, which in turn
+    /// resulted in attempt to read into a buffer of zero size.
+    /// When poll_read() reads into a buffer of zero size,
+    /// it can only read zero bytes, which is indistinguishable
+    /// from EOF and resulted in an erroneous detection of EOF
+    /// when in fact the stream was not closed.
+    #[test]
+    fn test_ensure_capacity_loop() {
+        let mut buf = Buffer::new();
+
+        for i in 1..500 {
+            // Ask for `i` bytes.
+            buf.ensure_capacity(i).unwrap();
+
+            // Test that we can read at least as much as requested.
+            let free = buf.free_as_mut_slice();
+            let used = free.len();
+            assert!(used >= i);
+
+            // Use as much as allowed.
+            buf.extend_used(used);
+        }
+    }
+
+    #[test]
+    fn test_buffer_take_and_return_block() {
+        // This test identifies blocks by their size.
+        let mut buf = Buffer::new();
+        buf.grow(1).unwrap();
+        let block_size = buf.block.len();
+
+        let block = buf.take_block();
+        assert_eq!(block.len(), block_size);
+        assert_ne!(buf.block.len(), block_size);
+
+        buf.return_block(block);
+        assert_eq!(buf.block.len(), block_size);
+    }
+
+    #[test]
+    fn test_buffer_reset_with_data() {
+        // This test identifies blocks by their size.
+        let data: [u8; 2 * Buffer::BLOCK_SIZE] = [b'a'; 2 * Buffer::BLOCK_SIZE];
+        let mut buf = Buffer::new();
+        let block_size = buf.block.len();
+        assert_eq!(block_size, Buffer::BLOCK_SIZE);
+        buf.reset_with_data(&data);
+        assert_ne!(buf.block.len(), block_size);
+        assert_eq!(buf.block.len(), 3 * Buffer::BLOCK_SIZE);
+        assert!(!buf.free_as_mut_slice().is_empty());
+
+        let data: [u8; 0] = [];
+        let mut buf = Buffer::new();
+        buf.reset_with_data(&data);
+        assert_eq!(buf.block.len(), Buffer::BLOCK_SIZE);
+    }
+
+    #[test]
+    fn test_buffer_debug() {
+        assert_eq!(
+            format!("{:?}", Buffer::new()),
+            format!(r#"Buffer {{ used: 0, capacity: {} }}"#, Buffer::BLOCK_SIZE)
+        );
+    }
+}

+ 102 - 0
async-imap-wasi/src/lib.rs

@@ -0,0 +1,102 @@
+//! # Async IMAP
+//!
+//! This crate lets you connect to and interact with servers
+//! that implement the IMAP protocol ([RFC 3501](https://tools.ietf.org/html/rfc3501) and extensions).
+//! After authenticating with the server,
+//! IMAP lets you list, fetch, and search for e-mails,
+//! as well as monitor mailboxes for changes.
+//!
+//! ## Connecting
+//!
+//! Connect to the server, for example using TLS connection on port 993
+//! or plain TCP connection on port 143 if you plan to use STARTTLS.
+//! can be used.
+//! Pass the stream to [`Client::new()`].
+//! This gives you an unauthenticated [`Client`].
+//!
+//! Then read the server greeting:
+//! ```ignore
+//! let _greeting = client
+//!     .read_response().await?
+//!     .expect("unexpected end of stream, expected greeting");
+//! ```
+//!
+//! ## STARTTLS
+//!
+//! If you connected on a non-TLS port, upgrade the connection using STARTTLS:
+//! ```ignore
+//! client.run_command_and_check_ok("STARTTLS", None).await?;
+//! let stream = client.into_inner();
+//! ```
+//! Convert this stream into a TLS stream using a library
+//! such as [`async-native-tls`](https://crates.io/crates/async-native-tls)
+//! or [Rustls](`https://crates.io/crates/rustls`).
+//! Once you have a TLS stream, wrap it back into a [`Client`]:
+//! ```ignore
+//! let client = Client::new(tls_stream);
+//! ```
+//! Note that there is no server greeting after STARTTLS.
+//!
+//! ## Authentication and session usage
+//!
+//! Once you have an established connection,
+//! authenticate using [`Client::login`] or [`Client::authenticate`]
+//! to perform username/password or challenge/response authentication respectively.
+//! This in turn gives you an authenticated
+//! [`Session`], which lets you access the mailboxes at the server.
+//! For example:
+//! ```ignore
+//! let mut session = client
+//!     .login("alice@example.org", "password").await
+//!     .map_err(|(err, _client)| err)?;
+//! session.select("INBOX").await?;
+//!
+//! // Fetch message number 1 in this mailbox, along with its RFC 822 field.
+//! // RFC 822 dictates the format of the body of e-mails.
+//! let messages_stream = imap_session.fetch("1", "RFC822").await?;
+//! let messages: Vec<_> = messages_stream.try_collect().await?;
+//! let message = messages.first().expect("found no messages in the INBOX");
+//!
+//! // Extract the message body.
+//! let body = message.body().expect("message did not have a body!");
+//! let body = std::str::from_utf8(body)
+//!     .expect("message was not valid utf-8")
+//!     .to_string();
+//!
+//! session.logout().await?;
+//! ```
+//!
+//! The documentation within this crate borrows heavily from the various RFCs,
+//! but should not be considered a complete reference.
+//! If anything is unclear,
+//! follow the links to the RFCs embedded in the documentation
+//! for the various types and methods and read the raw text there!
+//!
+//! See the `examples/` directory for usage examples.
+#![warn(missing_docs)]
+#![deny(rust_2018_idioms, unsafe_code)]
+
+#[cfg(not(any(feature = "runtime-tokio", feature = "runtime-async-std")))]
+compile_error!("one of 'runtime-async-std' or 'runtime-tokio' features must be enabled");
+
+#[cfg(all(feature = "runtime-tokio", feature = "runtime-async-std"))]
+compile_error!("only one of 'runtime-async-std' or 'runtime-tokio' features must be enabled");
+#[macro_use]
+extern crate pin_utils;
+
+// Reexport imap_proto for easier access.
+pub use imap_proto;
+
+mod authenticator;
+mod client;
+pub mod error;
+pub mod extensions;
+mod imap_stream;
+mod parse;
+pub mod types;
+
+pub use crate::authenticator::Authenticator;
+pub use crate::client::*;
+
+#[cfg(test)]
+mod mock_stream;

+ 143 - 0
async-imap-wasi/src/mock_stream.rs

@@ -0,0 +1,143 @@
+use std::cmp::min;
+use std::io::{Error, ErrorKind, Result};
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+#[cfg(feature = "runtime-async-std")]
+use async_std::io::{Read, Write};
+#[cfg(feature = "runtime-tokio")]
+use tokio::io::{AsyncRead as Read, AsyncWrite as Write};
+
+#[derive(Default, Clone, Debug, Eq, PartialEq, Hash)]
+pub struct MockStream {
+    read_buf: Vec<u8>,
+    read_pos: usize,
+    pub written_buf: Vec<u8>,
+    err_on_read: bool,
+    eof_on_read: bool,
+    read_delay: usize,
+}
+
+impl MockStream {
+    pub fn new(read_buf: Vec<u8>) -> MockStream {
+        MockStream::default().with_buf(read_buf)
+    }
+
+    pub fn with_buf(mut self, read_buf: Vec<u8>) -> MockStream {
+        self.read_buf = read_buf;
+        self
+    }
+
+    pub fn with_eof(mut self) -> MockStream {
+        self.eof_on_read = true;
+        self
+    }
+
+    pub fn with_err(mut self) -> MockStream {
+        self.err_on_read = true;
+        self
+    }
+
+    pub fn with_delay(mut self) -> MockStream {
+        self.read_delay = 1;
+        self
+    }
+}
+
+#[cfg(feature = "runtime-tokio")]
+impl Read for MockStream {
+    fn poll_read(
+        mut self: Pin<&mut Self>,
+        _cx: &mut Context<'_>,
+        buf: &mut tokio::io::ReadBuf<'_>,
+    ) -> Poll<Result<()>> {
+        if self.eof_on_read {
+            return Poll::Ready(Ok(()));
+        }
+        if self.err_on_read {
+            return Poll::Ready(Err(Error::new(ErrorKind::Other, "MockStream Error")));
+        }
+        if self.read_pos >= self.read_buf.len() {
+            return Poll::Ready(Err(Error::new(ErrorKind::UnexpectedEof, "EOF")));
+        }
+        let mut write_len = min(buf.remaining(), self.read_buf.len() - self.read_pos);
+        if self.read_delay > 0 {
+            self.read_delay -= 1;
+            write_len = min(write_len, 1);
+        }
+        let max_pos = self.read_pos + write_len;
+        buf.put_slice(&self.read_buf[self.read_pos..max_pos]);
+        self.read_pos += write_len;
+        Poll::Ready(Ok(()))
+    }
+}
+
+#[cfg(feature = "runtime-tokio")]
+impl Write for MockStream {
+    fn poll_write(
+        mut self: Pin<&mut Self>,
+        _cx: &mut Context<'_>,
+        buf: &[u8],
+    ) -> Poll<Result<usize>> {
+        self.written_buf.extend_from_slice(buf);
+        Poll::Ready(Ok(buf.len()))
+    }
+
+    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<()>> {
+        Poll::Ready(Ok(()))
+    }
+
+    fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
+        Poll::Ready(Ok(()))
+    }
+}
+
+#[cfg(feature = "runtime-async-std")]
+impl Read for MockStream {
+    fn poll_read(
+        mut self: Pin<&mut Self>,
+        _cx: &mut Context<'_>,
+        buf: &mut [u8],
+    ) -> Poll<Result<usize>> {
+        if self.eof_on_read {
+            return Poll::Ready(Ok(0));
+        }
+        if self.err_on_read {
+            return Poll::Ready(Err(Error::new(ErrorKind::Other, "MockStream Error")));
+        }
+        if self.read_pos >= self.read_buf.len() {
+            return Poll::Ready(Err(Error::new(ErrorKind::UnexpectedEof, "EOF")));
+        }
+        let mut write_len = min(buf.len(), self.read_buf.len() - self.read_pos);
+        if self.read_delay > 0 {
+            self.read_delay -= 1;
+            write_len = min(write_len, 1);
+        }
+        let max_pos = self.read_pos + write_len;
+        for x in self.read_pos..max_pos {
+            buf[x - self.read_pos] = self.read_buf[x];
+        }
+        self.read_pos += write_len;
+        Poll::Ready(Ok(write_len))
+    }
+}
+
+#[cfg(feature = "runtime-async-std")]
+impl Write for MockStream {
+    fn poll_write(
+        mut self: Pin<&mut Self>,
+        _cx: &mut Context<'_>,
+        buf: &[u8],
+    ) -> Poll<Result<usize>> {
+        self.written_buf.extend_from_slice(buf);
+        Poll::Ready(Ok(buf.len()))
+    }
+
+    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<()>> {
+        Poll::Ready(Ok(()))
+    }
+
+    fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<()>> {
+        Poll::Ready(Ok(()))
+    }
+}

+ 798 - 0
async-imap-wasi/src/parse.rs

@@ -0,0 +1,798 @@
+use std::collections::HashSet;
+
+use async_channel as channel;
+use futures::io;
+use futures::prelude::*;
+use futures::stream::Stream;
+use imap_proto::{self, MailboxDatum, Metadata, RequestId, Response};
+
+use crate::error::{Error, Result};
+use crate::types::ResponseData;
+use crate::types::*;
+
+pub(crate) fn parse_names<T: Stream<Item = io::Result<ResponseData>> + Unpin + Send>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> impl Stream<Item = Result<Name>> + '_ + Send + Unpin {
+    use futures::{FutureExt, StreamExt};
+
+    StreamExt::filter_map(
+        StreamExt::take_while(stream, move |res| filter(res, &command_tag)),
+        move |resp| {
+            let unsolicited = unsolicited.clone();
+            async move {
+                match resp {
+                    Ok(resp) => match resp.parsed() {
+                        Response::MailboxData(MailboxDatum::List { .. }) => {
+                            let name = Name::from_mailbox_data(resp);
+                            Some(Ok(name))
+                        }
+                        _ => {
+                            handle_unilateral(resp, unsolicited).await;
+                            None
+                        }
+                    },
+                    Err(err) => Some(Err(err.into())),
+                }
+            }
+            .boxed()
+        },
+    )
+}
+
+pub(crate) fn filter(
+    res: &io::Result<ResponseData>,
+    command_tag: &RequestId,
+) -> impl Future<Output = bool> {
+    let val = filter_sync(res, command_tag);
+    futures::future::ready(val)
+}
+
+pub(crate) fn filter_sync(res: &io::Result<ResponseData>, command_tag: &RequestId) -> bool {
+    match res {
+        Ok(res) => match res.parsed() {
+            Response::Done { tag, .. } => tag != command_tag,
+            _ => true,
+        },
+        Err(_err) => {
+            // Do not filter out the errors such as unexpected EOF.
+            true
+        }
+    }
+}
+
+pub(crate) fn parse_fetches<T: Stream<Item = io::Result<ResponseData>> + Unpin + Send>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> impl Stream<Item = Result<Fetch>> + '_ + Send + Unpin {
+    use futures::{FutureExt, StreamExt};
+
+    StreamExt::filter_map(
+        StreamExt::take_while(stream, move |res| filter(res, &command_tag)),
+        move |resp| {
+            let unsolicited = unsolicited.clone();
+
+            async move {
+                match resp {
+                    Ok(resp) => match resp.parsed() {
+                        Response::Fetch(..) => Some(Ok(Fetch::new(resp))),
+                        _ => {
+                            handle_unilateral(resp, unsolicited).await;
+                            None
+                        }
+                    },
+                    Err(err) => Some(Err(err.into())),
+                }
+            }
+            .boxed()
+        },
+    )
+}
+
+pub(crate) async fn parse_status<T: Stream<Item = io::Result<ResponseData>> + Unpin + Send>(
+    stream: &mut T,
+    expected_mailbox: &str,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> Result<Mailbox> {
+    let mut mbox = Mailbox::default();
+
+    while let Some(resp) = stream.next().await {
+        let resp = resp?;
+        match resp.parsed() {
+            Response::Done {
+                tag,
+                status,
+                code,
+                information,
+                ..
+            } if tag == &command_tag => {
+                use imap_proto::Status;
+                match status {
+                    Status::Ok => {
+                        break;
+                    }
+                    Status::Bad => {
+                        return Err(Error::Bad(format!(
+                            "code: {:?}, info: {:?}",
+                            code, information
+                        )))
+                    }
+                    Status::No => {
+                        return Err(Error::No(format!(
+                            "code: {:?}, info: {:?}",
+                            code, information
+                        )))
+                    }
+                    _ => {
+                        return Err(Error::Io(io::Error::new(
+                            io::ErrorKind::Other,
+                            format!(
+                                "status: {:?}, code: {:?}, information: {:?}",
+                                status, code, information
+                            ),
+                        )));
+                    }
+                }
+            }
+            Response::MailboxData(MailboxDatum::Status { mailbox, status })
+                if mailbox == expected_mailbox =>
+            {
+                for attribute in status {
+                    match attribute {
+                        StatusAttribute::HighestModSeq(highest_modseq) => {
+                            mbox.highest_modseq = Some(*highest_modseq)
+                        }
+                        StatusAttribute::Messages(exists) => mbox.exists = *exists,
+                        StatusAttribute::Recent(recent) => mbox.recent = *recent,
+                        StatusAttribute::UidNext(uid_next) => mbox.uid_next = Some(*uid_next),
+                        StatusAttribute::UidValidity(uid_validity) => {
+                            mbox.uid_validity = Some(*uid_validity)
+                        }
+                        StatusAttribute::Unseen(unseen) => mbox.unseen = Some(*unseen),
+                        _ => {}
+                    }
+                }
+            }
+            _ => {
+                handle_unilateral(resp, unsolicited.clone()).await;
+            }
+        }
+    }
+
+    Ok(mbox)
+}
+
+pub(crate) fn parse_expunge<T: Stream<Item = io::Result<ResponseData>> + Unpin + Send>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> impl Stream<Item = Result<u32>> + '_ + Send {
+    use futures::StreamExt;
+
+    StreamExt::filter_map(
+        StreamExt::take_while(stream, move |res| filter(res, &command_tag)),
+        move |resp| {
+            let unsolicited = unsolicited.clone();
+
+            async move {
+                match resp {
+                    Ok(resp) => match resp.parsed() {
+                        Response::Expunge(id) => Some(Ok(*id)),
+                        _ => {
+                            handle_unilateral(resp, unsolicited).await;
+                            None
+                        }
+                    },
+                    Err(err) => Some(Err(err.into())),
+                }
+            }
+        },
+    )
+}
+
+pub(crate) async fn parse_capabilities<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> Result<Capabilities> {
+    let mut caps: HashSet<Capability> = HashSet::new();
+
+    while let Some(resp) = stream
+        .take_while(|res| filter(res, &command_tag))
+        .next()
+        .await
+    {
+        let resp = resp?;
+        match resp.parsed() {
+            Response::Capabilities(cs) => {
+                for c in cs {
+                    caps.insert(Capability::from(c)); // TODO: avoid clone
+                }
+            }
+            _ => {
+                handle_unilateral(resp, unsolicited.clone()).await;
+            }
+        }
+    }
+
+    Ok(Capabilities(caps))
+}
+
+pub(crate) async fn parse_noop<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> Result<()> {
+    while let Some(resp) = stream
+        .take_while(|res| filter(res, &command_tag))
+        .next()
+        .await
+    {
+        let resp = resp?;
+        handle_unilateral(resp, unsolicited.clone()).await;
+    }
+
+    Ok(())
+}
+
+pub(crate) async fn parse_mailbox<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> Result<Mailbox> {
+    let mut mailbox = Mailbox::default();
+
+    while let Some(resp) = stream.next().await {
+        let resp = resp?;
+        match resp.parsed() {
+            Response::Done {
+                tag,
+                status,
+                code,
+                information,
+                ..
+            } if tag == &command_tag => {
+                use imap_proto::Status;
+                match status {
+                    Status::Ok => {
+                        break;
+                    }
+                    Status::Bad => {
+                        return Err(Error::Bad(format!(
+                            "code: {:?}, info: {:?}",
+                            code, information
+                        )))
+                    }
+                    Status::No => {
+                        return Err(Error::No(format!(
+                            "code: {:?}, info: {:?}",
+                            code, information
+                        )))
+                    }
+                    _ => {
+                        return Err(Error::Io(io::Error::new(
+                            io::ErrorKind::Other,
+                            format!(
+                                "status: {:?}, code: {:?}, information: {:?}",
+                                status, code, information
+                            ),
+                        )));
+                    }
+                }
+            }
+            Response::Data {
+                status,
+                code,
+                information,
+            } => {
+                use imap_proto::Status;
+
+                match status {
+                    Status::Ok => {
+                        use imap_proto::ResponseCode;
+                        match code {
+                            Some(ResponseCode::UidValidity(uid)) => {
+                                mailbox.uid_validity = Some(*uid);
+                            }
+                            Some(ResponseCode::UidNext(unext)) => {
+                                mailbox.uid_next = Some(*unext);
+                            }
+                            Some(ResponseCode::HighestModSeq(highest_modseq)) => {
+                                mailbox.highest_modseq = Some(*highest_modseq);
+                            }
+                            Some(ResponseCode::Unseen(n)) => {
+                                mailbox.unseen = Some(*n);
+                            }
+                            Some(ResponseCode::PermanentFlags(flags)) => {
+                                mailbox
+                                    .permanent_flags
+                                    .extend(flags.iter().map(|s| (*s).to_string()).map(Flag::from));
+                            }
+                            _ => {}
+                        }
+                    }
+                    Status::Bad => {
+                        return Err(Error::Bad(format!(
+                            "code: {:?}, info: {:?}",
+                            code, information
+                        )))
+                    }
+                    Status::No => {
+                        return Err(Error::No(format!(
+                            "code: {:?}, info: {:?}",
+                            code, information
+                        )))
+                    }
+                    _ => {
+                        return Err(Error::Io(io::Error::new(
+                            io::ErrorKind::Other,
+                            format!(
+                                "status: {:?}, code: {:?}, information: {:?}",
+                                status, code, information
+                            ),
+                        )));
+                    }
+                }
+            }
+            Response::MailboxData(m) => match m {
+                MailboxDatum::Status { .. } => handle_unilateral(resp, unsolicited.clone()).await,
+                MailboxDatum::Exists(e) => {
+                    mailbox.exists = *e;
+                }
+                MailboxDatum::Recent(r) => {
+                    mailbox.recent = *r;
+                }
+                MailboxDatum::Flags(flags) => {
+                    mailbox
+                        .flags
+                        .extend(flags.iter().map(|s| (*s).to_string()).map(Flag::from));
+                }
+                MailboxDatum::List { .. } => {}
+                MailboxDatum::MetadataSolicited { .. } => {}
+                MailboxDatum::MetadataUnsolicited { .. } => {}
+                MailboxDatum::Search { .. } => {}
+                MailboxDatum::Sort { .. } => {}
+                _ => {}
+            },
+            _ => {
+                handle_unilateral(resp, unsolicited.clone()).await;
+            }
+        }
+    }
+
+    Ok(mailbox)
+}
+
+pub(crate) async fn parse_ids<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
+    stream: &mut T,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> Result<HashSet<u32>> {
+    let mut ids: HashSet<u32> = HashSet::new();
+
+    while let Some(resp) = stream
+        .take_while(|res| filter(res, &command_tag))
+        .next()
+        .await
+    {
+        let resp = resp?;
+        match resp.parsed() {
+            Response::MailboxData(MailboxDatum::Search(cs)) => {
+                for c in cs {
+                    ids.insert(*c);
+                }
+            }
+            _ => {
+                handle_unilateral(resp, unsolicited.clone()).await;
+            }
+        }
+    }
+
+    Ok(ids)
+}
+
+/// Parses [GETMETADATA](https://www.rfc-editor.org/info/rfc5464) response.
+pub(crate) async fn parse_metadata<T: Stream<Item = io::Result<ResponseData>> + Unpin>(
+    stream: &mut T,
+    mailbox_name: &str,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+    command_tag: RequestId,
+) -> Result<Vec<Metadata>> {
+    let mut res_values = Vec::new();
+    while let Some(resp) = stream
+        .take_while(|res| filter(res, &command_tag))
+        .next()
+        .await
+    {
+        let resp = resp?;
+        match resp.parsed() {
+            // METADATA Response with Values
+            // <https://datatracker.ietf.org/doc/html/rfc5464.html#section-4.4.1>
+            Response::MailboxData(MailboxDatum::MetadataSolicited { mailbox, values })
+                if mailbox == mailbox_name =>
+            {
+                res_values.extend_from_slice(values.as_slice());
+            }
+
+            // We are not interested in
+            // [Unsolicited METADATA Response without Values](https://datatracker.ietf.org/doc/html/rfc5464.html#section-4.4.2),
+            // they go to unsolicited channel with other unsolicited responses.
+            _ => {
+                handle_unilateral(resp, unsolicited.clone()).await;
+            }
+        }
+    }
+    Ok(res_values)
+}
+
+// check if this is simply a unilateral server response
+// (see Section 7 of RFC 3501):
+pub(crate) async fn handle_unilateral(
+    res: ResponseData,
+    unsolicited: channel::Sender<UnsolicitedResponse>,
+) {
+    // ignore these if they are not being consumed
+    if unsolicited.is_full() {
+        return;
+    }
+
+    match res.parsed() {
+        Response::MailboxData(MailboxDatum::Status { mailbox, status }) => {
+            unsolicited
+                .send(UnsolicitedResponse::Status {
+                    mailbox: (mailbox.as_ref()).into(),
+                    attributes: status.to_vec(),
+                })
+                .await
+                .expect("Channel closed unexpectedly");
+        }
+        Response::MailboxData(MailboxDatum::Recent(n)) => {
+            unsolicited
+                .send(UnsolicitedResponse::Recent(*n))
+                .await
+                .expect("Channel closed unexpectedly");
+        }
+        Response::MailboxData(MailboxDatum::Exists(n)) => {
+            unsolicited
+                .send(UnsolicitedResponse::Exists(*n))
+                .await
+                .expect("Channel closed unexpectedly");
+        }
+        Response::Expunge(n) => {
+            unsolicited
+                .send(UnsolicitedResponse::Expunge(*n))
+                .await
+                .expect("Channel closed unexpectedly");
+        }
+        _ => {
+            unsolicited
+                .send(UnsolicitedResponse::Other(res))
+                .await
+                .expect("Channel closed unexpectedly");
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use async_channel::bounded;
+    use bytes::BytesMut;
+
+    fn input_stream(data: &[&str]) -> Vec<io::Result<ResponseData>> {
+        data.iter()
+            .map(|line| {
+                let block = BytesMut::from(line.as_bytes());
+                ResponseData::try_new(block, |bytes| -> io::Result<_> {
+                    let (remaining, response) = imap_proto::parser::parse_response(bytes).unwrap();
+                    assert_eq!(remaining.len(), 0);
+                    Ok(response)
+                })
+            })
+            .collect()
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_capability_test() {
+        let expected_capabilities = &["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"];
+        let responses =
+            input_stream(&["* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"]);
+
+        let mut stream = async_std::stream::from_iter(responses);
+        let (send, recv) = bounded(10);
+        let id = RequestId("A0001".into());
+        let capabilities = parse_capabilities(&mut stream, send, id).await.unwrap();
+        // shouldn't be any unexpected responses parsed
+        assert!(recv.is_empty());
+        assert_eq!(capabilities.len(), 4);
+        for e in expected_capabilities {
+            assert!(capabilities.has_str(e));
+        }
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_capability_case_insensitive_test() {
+        // Test that "IMAP4REV1" (instead of "IMAP4rev1") is accepted
+        let expected_capabilities = &["IMAP4rev1", "STARTTLS"];
+        let responses = input_stream(&["* CAPABILITY IMAP4REV1 STARTTLS\r\n"]);
+        let mut stream = async_std::stream::from_iter(responses);
+
+        let (send, recv) = bounded(10);
+        let id = RequestId("A0001".into());
+        let capabilities = parse_capabilities(&mut stream, send, id).await.unwrap();
+
+        // shouldn't be any unexpected responses parsed
+        assert!(recv.is_empty());
+        assert_eq!(capabilities.len(), 2);
+        for e in expected_capabilities {
+            assert!(capabilities.has_str(e));
+        }
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    #[should_panic]
+    async fn parse_capability_invalid_test() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&["* JUNK IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n"]);
+        let mut stream = async_std::stream::from_iter(responses);
+
+        let id = RequestId("A0001".into());
+        parse_capabilities(&mut stream, send.clone(), id)
+            .await
+            .unwrap();
+        assert!(recv.is_empty());
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_names_test() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&["* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n"]);
+        let mut stream = async_std::stream::from_iter(responses);
+
+        let id = RequestId("A0001".into());
+        let names: Vec<_> = parse_names(&mut stream, send, id)
+            .try_collect::<Vec<Name>>()
+            .await
+            .unwrap();
+        assert!(recv.is_empty());
+        assert_eq!(names.len(), 1);
+        assert_eq!(
+            names[0].attributes(),
+            &[NameAttribute::Extension("\\HasNoChildren".into())]
+        );
+        assert_eq!(names[0].delimiter(), Some("."));
+        assert_eq!(names[0].name(), "INBOX");
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_fetches_empty() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&[]);
+        let mut stream = async_std::stream::from_iter(responses);
+        let id = RequestId("a".into());
+
+        let fetches = parse_fetches(&mut stream, send, id)
+            .try_collect::<Vec<_>>()
+            .await
+            .unwrap();
+        assert!(recv.is_empty());
+        assert!(fetches.is_empty());
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_fetches_test() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&[
+            "* 24 FETCH (FLAGS (\\Seen) UID 4827943)\r\n",
+            "* 25 FETCH (FLAGS (\\Seen))\r\n",
+        ]);
+        let mut stream = async_std::stream::from_iter(responses);
+        let id = RequestId("a".into());
+
+        let fetches = parse_fetches(&mut stream, send, id)
+            .try_collect::<Vec<_>>()
+            .await
+            .unwrap();
+        assert!(recv.is_empty());
+
+        assert_eq!(fetches.len(), 2);
+        assert_eq!(fetches[0].message, 24);
+        assert_eq!(fetches[0].flags().collect::<Vec<_>>(), vec![Flag::Seen]);
+        assert_eq!(fetches[0].uid, Some(4827943));
+        assert_eq!(fetches[0].body(), None);
+        assert_eq!(fetches[0].header(), None);
+        assert_eq!(fetches[1].message, 25);
+        assert_eq!(fetches[1].flags().collect::<Vec<_>>(), vec![Flag::Seen]);
+        assert_eq!(fetches[1].uid, None);
+        assert_eq!(fetches[1].body(), None);
+        assert_eq!(fetches[1].header(), None);
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_fetches_w_unilateral() {
+        // https://github.com/mattnenterprise/rust-imap/issues/81
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&["* 37 FETCH (UID 74)\r\n", "* 1 RECENT\r\n"]);
+        let mut stream = async_std::stream::from_iter(responses);
+        let id = RequestId("a".into());
+
+        let fetches = parse_fetches(&mut stream, send, id)
+            .try_collect::<Vec<_>>()
+            .await
+            .unwrap();
+        assert_eq!(recv.recv().await.unwrap(), UnsolicitedResponse::Recent(1));
+
+        assert_eq!(fetches.len(), 1);
+        assert_eq!(fetches[0].message, 37);
+        assert_eq!(fetches[0].uid, Some(74));
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_names_w_unilateral() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&[
+            "* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n",
+            "* 4 EXPUNGE\r\n",
+        ]);
+        let mut stream = async_std::stream::from_iter(responses);
+
+        let id = RequestId("A0001".into());
+        let names = parse_names(&mut stream, send, id)
+            .try_collect::<Vec<_>>()
+            .await
+            .unwrap();
+
+        assert_eq!(recv.recv().await.unwrap(), UnsolicitedResponse::Expunge(4));
+
+        assert_eq!(names.len(), 1);
+        assert_eq!(
+            names[0].attributes(),
+            &[NameAttribute::Extension("\\HasNoChildren".into())]
+        );
+        assert_eq!(names[0].delimiter(), Some("."));
+        assert_eq!(names[0].name(), "INBOX");
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_capabilities_w_unilateral() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&[
+            "* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED\r\n",
+            "* STATUS dev.github (MESSAGES 10 UIDNEXT 11 UIDVALIDITY 1408806928 UNSEEN 0)\r\n",
+            "* 4 EXISTS\r\n",
+        ]);
+        let mut stream = async_std::stream::from_iter(responses);
+
+        let expected_capabilities = &["IMAP4rev1", "STARTTLS", "AUTH=GSSAPI", "LOGINDISABLED"];
+
+        let id = RequestId("A0001".into());
+        let capabilities = parse_capabilities(&mut stream, send, id).await.unwrap();
+
+        assert_eq!(capabilities.len(), 4);
+        for e in expected_capabilities {
+            assert!(capabilities.has_str(e));
+        }
+
+        assert_eq!(
+            recv.recv().await.unwrap(),
+            UnsolicitedResponse::Status {
+                mailbox: "dev.github".to_string(),
+                attributes: vec![
+                    StatusAttribute::Messages(10),
+                    StatusAttribute::UidNext(11),
+                    StatusAttribute::UidValidity(1408806928),
+                    StatusAttribute::Unseen(0)
+                ]
+            }
+        );
+        assert_eq!(recv.recv().await.unwrap(), UnsolicitedResponse::Exists(4));
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_ids_w_unilateral() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&[
+            "* SEARCH 23 42 4711\r\n",
+            "* 1 RECENT\r\n",
+            "* STATUS INBOX (MESSAGES 10 UIDNEXT 11 UIDVALIDITY 1408806928 UNSEEN 0)\r\n",
+        ]);
+        let mut stream = async_std::stream::from_iter(responses);
+
+        let id = RequestId("A0001".into());
+        let ids = parse_ids(&mut stream, send, id).await.unwrap();
+
+        assert_eq!(ids, [23, 42, 4711].iter().cloned().collect());
+
+        assert_eq!(recv.recv().await.unwrap(), UnsolicitedResponse::Recent(1));
+        assert_eq!(
+            recv.recv().await.unwrap(),
+            UnsolicitedResponse::Status {
+                mailbox: "INBOX".to_string(),
+                attributes: vec![
+                    StatusAttribute::Messages(10),
+                    StatusAttribute::UidNext(11),
+                    StatusAttribute::UidValidity(1408806928),
+                    StatusAttribute::Unseen(0)
+                ]
+            }
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_ids_test() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&[
+                "* 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",
+                "* 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",
+            ]);
+        let mut stream = async_std::stream::from_iter(responses);
+
+        let id = RequestId("A0001".into());
+        let ids = parse_ids(&mut stream, send, id).await.unwrap();
+
+        assert!(recv.is_empty());
+        let ids: HashSet<u32> = ids.iter().cloned().collect();
+        assert_eq!(
+            ids,
+            [
+                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, 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
+            ]
+            .iter()
+            .cloned()
+            .collect()
+        );
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_ids_search() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&["* SEARCH\r\n"]);
+        let mut stream = async_std::stream::from_iter(responses);
+
+        let id = RequestId("A0001".into());
+        let ids = parse_ids(&mut stream, send, id).await.unwrap();
+
+        assert!(recv.is_empty());
+        let ids: HashSet<u32> = ids.iter().cloned().collect();
+        assert_eq!(ids, HashSet::<u32>::new());
+    }
+
+    #[cfg_attr(feature = "runtime-tokio", tokio::test)]
+    #[cfg_attr(feature = "runtime-async-std", async_std::test)]
+    async fn parse_mailbox_does_not_exist_error() {
+        let (send, recv) = bounded(10);
+        let responses = input_stream(&[
+            "A0003 NO Mailbox doesn't exist: DeltaChat (0.001 + 0.140 + 0.139 secs).\r\n",
+        ]);
+        let mut stream = async_std::stream::from_iter(responses);
+
+        let id = RequestId("A0003".into());
+        let mailbox = parse_mailbox(&mut stream, send, id).await;
+        assert!(recv.is_empty());
+
+        assert!(matches!(mailbox, Err(Error::No(_))));
+    }
+}

+ 91 - 0
async-imap-wasi/src/types/capabilities.rs

@@ -0,0 +1,91 @@
+use imap_proto::types::Capability as CapabilityRef;
+use std::collections::hash_set::Iter;
+use std::collections::HashSet;
+
+const IMAP4REV1_CAPABILITY: &str = "IMAP4rev1";
+const AUTH_CAPABILITY_PREFIX: &str = "AUTH=";
+
+/// List of available Capabilities.
+#[derive(Debug, Eq, PartialEq, Hash)]
+pub enum Capability {
+    /// The crucial imap capability.
+    Imap4rev1,
+    /// Auth type capability.
+    Auth(String),
+    /// Any other atoms.
+    Atom(String),
+}
+
+impl From<&CapabilityRef<'_>> for Capability {
+    fn from(c: &CapabilityRef<'_>) -> Self {
+        match c {
+            CapabilityRef::Imap4rev1 => Capability::Imap4rev1,
+            CapabilityRef::Auth(s) => Capability::Auth(s.clone().into_owned()),
+            CapabilityRef::Atom(s) => Capability::Atom(s.clone().into_owned()),
+        }
+    }
+}
+
+/// From [section 7.2.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-7.2.1).
+///
+/// A list of capabilities that the server supports.
+/// The capability list will include the atom "IMAP4rev1".
+///
+/// In addition, all servers implement the `STARTTLS`, `LOGINDISABLED`, and `AUTH=PLAIN` (described
+/// in [IMAP-TLS](https://tools.ietf.org/html/rfc2595)) capabilities. See the [Security
+/// Considerations section of the RFC](https://tools.ietf.org/html/rfc3501#section-11) for
+/// important information.
+///
+/// A capability name which begins with `AUTH=` indicates that the server supports that particular
+/// authentication mechanism.
+///
+/// The `LOGINDISABLED` capability indicates that the `LOGIN` command is disabled, and that the
+/// server will respond with a [`crate::error::Error::No`] response to any attempt to use the `LOGIN`
+/// command even if the user name and password are valid.  An IMAP client MUST NOT issue the
+/// `LOGIN` command if the server advertises the `LOGINDISABLED` capability.
+///
+/// Other capability names indicate that the server supports an extension, revision, or amendment
+/// to the IMAP4rev1 protocol. Capability names either begin with `X` or they are standard or
+/// standards-track [RFC 3501](https://tools.ietf.org/html/rfc3501) extensions, revisions, or
+/// amendments registered with IANA.
+///
+/// Client implementations SHOULD NOT require any capability name other than `IMAP4rev1`, and MUST
+/// ignore any unknown capability names.
+pub struct Capabilities(pub(crate) HashSet<Capability>);
+
+impl Capabilities {
+    /// Check if the server has the given capability.
+    pub fn has(&self, cap: &Capability) -> bool {
+        self.0.contains(cap)
+    }
+
+    /// Check if the server has the given capability via str.
+    pub fn has_str<S: AsRef<str>>(&self, cap: S) -> bool {
+        let s = cap.as_ref();
+        if s.eq_ignore_ascii_case(IMAP4REV1_CAPABILITY) {
+            return self.has(&Capability::Imap4rev1);
+        }
+        if s.len() > AUTH_CAPABILITY_PREFIX.len() {
+            let (pre, val) = s.split_at(AUTH_CAPABILITY_PREFIX.len());
+            if pre.eq_ignore_ascii_case(AUTH_CAPABILITY_PREFIX) {
+                return self.has(&Capability::Auth(val.into())); // TODO: avoid clone
+            }
+        }
+        self.has(&Capability::Atom(s.into())) // TODO: avoid clone
+    }
+
+    /// Iterate over all the server's capabilities
+    pub fn iter(&self) -> Iter<'_, Capability> {
+        self.0.iter()
+    }
+
+    /// Returns how many capabilities the server has.
+    pub fn len(&self) -> usize {
+        self.0.len()
+    }
+
+    /// Returns true if the server purports to have no capabilities.
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}

+ 234 - 0
async-imap-wasi/src/types/fetch.rs

@@ -0,0 +1,234 @@
+use chrono::{DateTime, FixedOffset};
+use imap_proto::types::{
+    AttributeValue, BodyStructure, Envelope, MessageSection, Response, SectionPath,
+};
+
+use super::{Flag, Seq, Uid};
+use crate::types::ResponseData;
+
+/// Format of Date and Time as defined RFC3501.
+/// See `date-time` element in [Formal Syntax](https://tools.ietf.org/html/rfc3501#section-9)
+/// chapter of this RFC.
+const DATE_TIME_FORMAT: &str = "%d-%b-%Y %H:%M:%S %z";
+
+/// An IMAP [`FETCH` response](https://tools.ietf.org/html/rfc3501#section-7.4.2) that contains
+/// data about a particular message. This response occurs as the result of a `FETCH` or `STORE`
+/// command, as well as by unilateral server decision (e.g., flag updates).
+#[derive(Debug)]
+pub struct Fetch {
+    response: ResponseData,
+    /// The ordinal number of this message in its containing mailbox.
+    pub message: Seq,
+
+    /// A number expressing the unique identifier of the message.
+    /// Only present if `UID` was specified in the query argument to `FETCH` and the server
+    /// supports UIDs.
+    pub uid: Option<Uid>,
+
+    /// A number expressing the [RFC-2822](https://tools.ietf.org/html/rfc2822) size of the message.
+    /// Only present if `RFC822.SIZE` was specified in the query argument to `FETCH`.
+    pub size: Option<u32>,
+
+    /// A number expressing the [RFC-7162](https://tools.ietf.org/html/rfc7162) mod-sequence
+    /// of the message.
+    pub modseq: Option<u64>,
+}
+
+impl Fetch {
+    pub(crate) fn new(response: ResponseData) -> Self {
+        let (message, uid, size, modseq) =
+            if let Response::Fetch(message, attrs) = response.parsed() {
+                let mut uid = None;
+                let mut size = None;
+                let mut modseq = None;
+
+                for attr in attrs {
+                    match attr {
+                        AttributeValue::Uid(id) => uid = Some(*id),
+                        AttributeValue::Rfc822Size(sz) => size = Some(*sz),
+                        AttributeValue::ModSeq(ms) => modseq = Some(*ms),
+                        _ => {}
+                    }
+                }
+                (*message, uid, size, modseq)
+            } else {
+                unreachable!()
+            };
+
+        Fetch {
+            response,
+            message,
+            uid,
+            size,
+            modseq,
+        }
+    }
+
+    /// A list of flags that are set for this message.
+    pub fn flags(&self) -> impl Iterator<Item = Flag<'_>> {
+        if let Response::Fetch(_, attrs) = self.response.parsed() {
+            attrs
+                .iter()
+                .filter_map(|attr| match attr {
+                    AttributeValue::Flags(raw_flags) => {
+                        Some(raw_flags.iter().map(|s| Flag::from(s.as_ref())))
+                    }
+                    _ => None,
+                })
+                .flatten()
+        } else {
+            unreachable!()
+        }
+    }
+
+    /// The bytes that make up the header of this message, if `BODY[HEADER]`, `BODY.PEEK[HEADER]`,
+    /// or `RFC822.HEADER` was included in the `query` argument to `FETCH`.
+    pub fn header(&self) -> Option<&[u8]> {
+        if let Response::Fetch(_, attrs) = self.response.parsed() {
+            attrs
+                .iter()
+                .filter_map(|av| match av {
+                    AttributeValue::BodySection {
+                        section: Some(SectionPath::Full(MessageSection::Header)),
+                        data: Some(hdr),
+                        ..
+                    }
+                    | AttributeValue::Rfc822Header(Some(hdr)) => Some(hdr.as_ref()),
+                    _ => None,
+                })
+                .next()
+        } else {
+            unreachable!()
+        }
+    }
+
+    /// The bytes that make up this message, included if `BODY[]` or `RFC822` was included in the
+    /// `query` argument to `FETCH`. The bytes SHOULD be interpreted by the client according to the
+    /// content transfer encoding, body type, and subtype.
+    pub fn body(&self) -> Option<&[u8]> {
+        if let Response::Fetch(_, attrs) = self.response.parsed() {
+            attrs
+                .iter()
+                .filter_map(|av| match av {
+                    AttributeValue::BodySection {
+                        section: None,
+                        data: Some(body),
+                        ..
+                    }
+                    | AttributeValue::Rfc822(Some(body)) => Some(body.as_ref()),
+                    _ => None,
+                })
+                .next()
+        } else {
+            unreachable!()
+        }
+    }
+
+    /// The bytes that make up the text of this message, included if `BODY[TEXT]`, `RFC822.TEXT`,
+    /// or `BODY.PEEK[TEXT]` was included in the `query` argument to `FETCH`. The bytes SHOULD be
+    /// interpreted by the client according to the content transfer encoding, body type, and
+    /// subtype.
+    pub fn text(&self) -> Option<&[u8]> {
+        if let Response::Fetch(_, attrs) = self.response.parsed() {
+            attrs
+                .iter()
+                .filter_map(|av| match av {
+                    AttributeValue::BodySection {
+                        section: Some(SectionPath::Full(MessageSection::Text)),
+                        data: Some(body),
+                        ..
+                    }
+                    | AttributeValue::Rfc822Text(Some(body)) => Some(body.as_ref()),
+                    _ => None,
+                })
+                .next()
+        } else {
+            unreachable!()
+        }
+    }
+
+    /// The envelope of this message, if `ENVELOPE` was included in the `query` argument to
+    /// `FETCH`. This is computed by the server by parsing the
+    /// [RFC-2822](https://tools.ietf.org/html/rfc2822) header into the component parts, defaulting
+    /// various fields as necessary.
+    ///
+    /// The full description of the format of the envelope is given in [RFC 3501 section
+    /// 7.4.2](https://tools.ietf.org/html/rfc3501#section-7.4.2).
+    pub fn envelope(&self) -> Option<&Envelope<'_>> {
+        if let Response::Fetch(_, attrs) = self.response.parsed() {
+            attrs
+                .iter()
+                .filter_map(|av| match av {
+                    AttributeValue::Envelope(env) => Some(&**env),
+                    _ => None,
+                })
+                .next()
+        } else {
+            unreachable!()
+        }
+    }
+
+    /// Extract the bytes that makes up the given `BOD[<section>]` of a `FETCH` response.
+    ///
+    /// See [section 7.4.2 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-7.4.2) for
+    /// details.
+    pub fn section(&self, path: &SectionPath) -> Option<&[u8]> {
+        if let Response::Fetch(_, attrs) = self.response.parsed() {
+            attrs
+                .iter()
+                .filter_map(|av| match av {
+                    AttributeValue::BodySection {
+                        section: Some(sp),
+                        data: Some(data),
+                        ..
+                    } if sp == path => Some(data.as_ref()),
+                    _ => None,
+                })
+                .next()
+        } else {
+            unreachable!()
+        }
+    }
+
+    /// Extract the `INTERNALDATE` of a `FETCH` response
+    ///
+    /// See [section 2.3.3 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.3) for
+    /// details.
+    pub fn internal_date(&self) -> Option<DateTime<FixedOffset>> {
+        if let Response::Fetch(_, attrs) = self.response.parsed() {
+            attrs
+                .iter()
+                .filter_map(|av| match av {
+                    AttributeValue::InternalDate(date_time) => Some(date_time.as_ref()),
+                    _ => None,
+                })
+                .next()
+                .and_then(
+                    |date_time| match DateTime::parse_from_str(date_time, DATE_TIME_FORMAT) {
+                        Ok(date_time) => Some(date_time),
+                        Err(_) => None,
+                    },
+                )
+        } else {
+            unreachable!()
+        }
+    }
+
+    /// Extract the `BODYSTRUCTURE` of a `FETCH` response
+    ///
+    /// See [section 2.3.6 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.6) for
+    /// details.
+    pub fn bodystructure(&self) -> Option<&BodyStructure<'_>> {
+        if let Response::Fetch(_, attrs) = self.response.parsed() {
+            attrs
+                .iter()
+                .filter_map(|av| match av {
+                    AttributeValue::BodyStructure(bs) => Some(bs),
+                    _ => None,
+                })
+                .next()
+        } else {
+            unreachable!()
+        }
+    }
+}

+ 29 - 0
async-imap-wasi/src/types/id_generator.rs

@@ -0,0 +1,29 @@
+use imap_proto::RequestId;
+
+/// Request ID generator.
+#[derive(Debug)]
+pub struct IdGenerator {
+    /// Last returned ID.
+    next: u64,
+}
+
+impl IdGenerator {
+    /// Creates a new request ID generator.
+    pub fn new() -> Self {
+        Self { next: 0 }
+    }
+}
+
+impl Default for IdGenerator {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Iterator for IdGenerator {
+    type Item = RequestId;
+    fn next(&mut self) -> Option<Self::Item> {
+        self.next += 1;
+        Some(RequestId(format!("A{:04}", self.next % 10_000)))
+    }
+}

+ 58 - 0
async-imap-wasi/src/types/mailbox.rs

@@ -0,0 +1,58 @@
+use super::{Flag, Uid};
+use std::fmt;
+
+/// Meta-information about an IMAP mailbox, as returned by
+/// [`SELECT`](https://tools.ietf.org/html/rfc3501#section-6.3.1) and friends.
+#[derive(Clone, Debug, Eq, PartialEq, Hash, Default)]
+pub struct Mailbox {
+    /// Defined flags in the mailbox.  See the description of the [FLAGS
+    /// response](https://tools.ietf.org/html/rfc3501#section-7.2.6) for more detail.
+    pub flags: Vec<Flag<'static>>,
+
+    /// The number of messages in the mailbox.  See the description of the [EXISTS
+    /// response](https://tools.ietf.org/html/rfc3501#section-7.3.1) for more detail.
+    pub exists: u32,
+
+    /// The number of messages with the \Recent flag set. See the description of the [RECENT
+    /// response](https://tools.ietf.org/html/rfc3501#section-7.3.2) for more detail.
+    pub recent: u32,
+
+    /// The message sequence number of the first unseen message in the mailbox.  If this is
+    /// missing, the client can not make any assumptions about the first unseen message in the
+    /// mailbox, and needs to issue a `SEARCH` command if it wants to find it.
+    pub unseen: Option<u32>,
+
+    /// A list of message flags that the client can change permanently.  If this is missing, the
+    /// client should assume that all flags can be changed permanently. If the client attempts to
+    /// STORE a flag that is not in this list list, the server will either ignore the change or
+    /// store the state change for the remainder of the current session only.
+    pub permanent_flags: Vec<Flag<'static>>,
+
+    /// The next unique identifier value.  If this is missing, the client can not make any
+    /// assumptions about the next unique identifier value.
+    pub uid_next: Option<Uid>,
+
+    /// The unique identifier validity value.  See [`Uid`] for more details.  If this is missing,
+    /// the server does not support unique identifiers.
+    pub uid_validity: Option<u32>,
+
+    /// Highest mailbox mod-sequence as defined in [RFC-7162](https://tools.ietf.org/html/rfc7162).
+    pub highest_modseq: Option<u64>,
+}
+
+impl fmt::Display for Mailbox {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "flags: {:?}, exists: {}, recent: {}, unseen: {:?}, permanent_flags: {:?},\
+             uid_next: {:?}, uid_validity: {:?}",
+            self.flags,
+            self.exists,
+            self.recent,
+            self.unseen,
+            self.permanent_flags,
+            self.uid_next,
+            self.uid_validity
+        )
+    }
+}

+ 279 - 0
async-imap-wasi/src/types/mod.rs

@@ -0,0 +1,279 @@
+//! This module contains types used throughout the IMAP protocol.
+
+use std::borrow::Cow;
+
+/// From section [2.3.1.1 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.1).
+///
+/// A 32-bit value assigned to each message, which when used with the unique identifier validity
+/// value (see below) forms a 64-bit value that will not refer to any other message in the mailbox
+/// or any subsequent mailbox with the same name forever.  Unique identifiers are assigned in a
+/// strictly ascending fashion in the mailbox; as each message is added to the mailbox it is
+/// assigned a higher UID than the message(s) which were added previously.  Unlike message sequence
+/// numbers, unique identifiers are not necessarily contiguous.
+///
+/// The unique identifier of a message will not change during the session, and will generally not
+/// change between sessions.  Any change of unique identifiers between sessions will be detectable
+/// using the `UIDVALIDITY` mechanism discussed below.  Persistent unique identifiers are required
+/// for a client to resynchronize its state from a previous session with the server (e.g.,
+/// disconnected or offline access clients); this is discussed further in
+/// [`IMAP-DISC`](https://tools.ietf.org/html/rfc3501#ref-IMAP-DISC).
+///
+/// Associated with every mailbox are two values which aid in unique identifier handling: the next
+/// unique identifier value and the unique identifier validity value.
+///
+/// The next unique identifier value is the predicted value that will be assigned to a new message
+/// in the mailbox.  Unless the unique identifier validity also changes (see below), the next
+/// unique identifier value will have the following two characteristics.  First, the next unique
+/// identifier value will not change unless new messages are added to the mailbox; and second, the
+/// next unique identifier value will change whenever new messages are added to the mailbox, even
+/// if those new messages are subsequently expunged.
+///
+/// > Note: The next unique identifier value is intended to provide a means for a client to
+/// > determine whether any messages have been delivered to the mailbox since the previous time it
+/// > checked this value.  It is not intended to provide any guarantee that any message will have
+/// > this unique identifier.  A client can only assume, at the time that it obtains the next
+/// > unique identifier value, that messages arriving after that time will have a UID greater than
+/// > or equal to that value.
+///
+/// The unique identifier validity value is sent in a `UIDVALIDITY` response code in an `OK`
+/// untagged response at mailbox selection time. If unique identifiers from an earlier session fail
+/// to persist in this session, the unique identifier validity value will be greater than the one
+/// used in the earlier session.
+///
+/// > Note: Ideally, unique identifiers will persist at all
+/// > times.  Although this specification recognizes that failure
+/// > to persist can be unavoidable in certain server
+/// > environments, it STRONGLY ENCOURAGES message store
+/// > implementation techniques that avoid this problem.  For
+/// > example:
+/// >
+/// >   1. Unique identifiers are strictly ascending in the
+/// >      mailbox at all times.  If the physical message store is
+/// >      re-ordered by a non-IMAP agent, this requires that the
+/// >      unique identifiers in the mailbox be regenerated, since
+/// >      the former unique identifiers are no longer strictly
+/// >      ascending as a result of the re-ordering.
+/// >   2. If the message store has no mechanism to store unique
+/// >      identifiers, it must regenerate unique identifiers at
+/// >      each session, and each session must have a unique
+/// >      `UIDVALIDITY` value.
+/// >   3. If the mailbox is deleted and a new mailbox with the
+/// >      same name is created at a later date, the server must
+/// >      either keep track of unique identifiers from the
+/// >      previous instance of the mailbox, or it must assign a
+/// >      new `UIDVALIDITY` value to the new instance of the
+/// >      mailbox.  A good `UIDVALIDITY` value to use in this case
+/// >      is a 32-bit representation of the creation date/time of
+/// >      the mailbox.  It is alright to use a constant such as
+/// >      1, but only if it guaranteed that unique identifiers
+/// >      will never be reused, even in the case of a mailbox
+/// >      being deleted (or renamed) and a new mailbox by the
+/// >      same name created at some future time.
+/// >   4. The combination of mailbox name, `UIDVALIDITY`, and `UID`
+/// >      must refer to a single immutable message on that server
+/// >      forever.  In particular, the internal date, [RFC 2822](https://tools.ietf.org/html/rfc2822)
+/// >      size, envelope, body structure, and message texts
+/// >      (RFC822, RFC822.HEADER, RFC822.TEXT, and all BODY[...]
+/// >      fetch data items) must never change.  This does not
+/// >      include message numbers, nor does it include attributes
+/// >      that can be set by a `STORE` command (e.g., `FLAGS`).
+pub type Uid = u32;
+
+/// From section [2.3.1.2 of RFC 3501](https://tools.ietf.org/html/rfc3501#section-2.3.1.2).
+///
+/// A relative position from 1 to the number of messages in the mailbox.
+/// This position is ordered by ascending unique identifier.  As
+/// each new message is added, it is assigned a message sequence number
+/// that is 1 higher than the number of messages in the mailbox before
+/// that new message was added.
+///
+/// Message sequence numbers can be reassigned during the session.  For
+/// example, when a message is permanently removed (expunged) from the
+/// mailbox, the message sequence number for all subsequent messages is
+/// decremented.  The number of messages in the mailbox is also
+/// decremented.  Similarly, a new message can be assigned a message
+/// sequence number that was once held by some other message prior to an
+/// expunge.
+///
+/// In addition to accessing messages by relative position in the
+/// mailbox, message sequence numbers can be used in mathematical
+/// calculations.  For example, if an untagged "11 EXISTS" is received,
+/// and previously an untagged "8 EXISTS" was received, three new
+/// messages have arrived with message sequence numbers of 9, 10, and 11.
+/// Another example, if message 287 in a 523 message mailbox has UID
+/// 12345, there are exactly 286 messages which have lesser UIDs and 236
+/// messages which have greater UIDs.
+pub type Seq = u32;
+
+/// With the exception of [`Flag::Custom`], these flags are system flags that are pre-defined in
+/// [RFC 3501 section 2.3.2](https://tools.ietf.org/html/rfc3501#section-2.3.2). All system flags
+/// begin with `\` in the IMAP protocol.  Certain system flags (`\Deleted` and `\Seen`) have
+/// special semantics described elsewhere.
+///
+/// A flag can be permanent or session-only on a per-flag basis. Permanent flags are those which
+/// the client can add or remove from the message flags permanently; that is, concurrent and
+/// subsequent sessions will see any change in permanent flags.  Changes to session flags are valid
+/// only in that session.
+///
+/// > Note: The `\Recent` system flag is a special case of a session flag.  `\Recent` can not be
+/// > used as an argument in a `STORE` or `APPEND` command, and thus can not be changed at all.
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub enum Flag<'a> {
+    /// Message has been read
+    Seen,
+
+    /// Message has been answered
+    Answered,
+
+    /// Message is "flagged" for urgent/special attention
+    Flagged,
+
+    /// Message is "deleted" for removal by later EXPUNGE
+    Deleted,
+
+    /// Message has not completed composition (marked as a draft).
+    Draft,
+
+    /// Message is "recently" arrived in this mailbox.  This session is the first session to have
+    /// been notified about this message; if the session is read-write, subsequent sessions will
+    /// not see `\Recent` set for this message.  This flag can not be altered by the client.
+    ///
+    /// If it is not possible to determine whether or not this session is the first session to be
+    /// notified about a message, then that message will generally be considered recent.
+    ///
+    /// If multiple connections have the same mailbox selected simultaneously, it is undefined
+    /// which of these connections will see newly-arrived messages with `\Recent` set and which
+    /// will see it without `\Recent` set.
+    Recent,
+
+    /// The [`Mailbox::permanent_flags`] can include this special flag (`\*`), which indicates that
+    /// it is possible to create new keywords by attempting to store those flags in the mailbox.
+    MayCreate,
+
+    /// A non-standard user- or server-defined flag.
+    Custom(Cow<'a, str>),
+}
+
+impl Flag<'static> {
+    fn system(s: &str) -> Option<Self> {
+        match s {
+            "\\Seen" => Some(Flag::Seen),
+            "\\Answered" => Some(Flag::Answered),
+            "\\Flagged" => Some(Flag::Flagged),
+            "\\Deleted" => Some(Flag::Deleted),
+            "\\Draft" => Some(Flag::Draft),
+            "\\Recent" => Some(Flag::Recent),
+            "\\*" => Some(Flag::MayCreate),
+            _ => None,
+        }
+    }
+}
+
+impl<'a> From<String> for Flag<'a> {
+    fn from(s: String) -> Self {
+        if let Some(f) = Flag::system(&s) {
+            f
+        } else {
+            Flag::Custom(Cow::Owned(s))
+        }
+    }
+}
+
+impl<'a> From<&'a str> for Flag<'a> {
+    fn from(s: &'a str) -> Self {
+        if let Some(f) = Flag::system(s) {
+            f
+        } else {
+            Flag::Custom(Cow::Borrowed(s))
+        }
+    }
+}
+
+mod mailbox;
+pub use self::mailbox::Mailbox;
+
+mod fetch;
+pub use self::fetch::Fetch;
+
+mod name;
+pub use self::name::{Name, NameAttribute};
+
+mod capabilities;
+pub use self::capabilities::{Capabilities, Capability};
+
+/// re-exported from imap_proto;
+pub use imap_proto::StatusAttribute;
+
+mod id_generator;
+pub(crate) use self::id_generator::IdGenerator;
+
+mod response_data;
+pub(crate) use self::response_data::ResponseData;
+
+mod request;
+pub(crate) use self::request::Request;
+
+mod quota;
+pub use self::quota::*;
+
+/// Responses that the server sends that are not related to the current command.
+/// [RFC 3501](https://tools.ietf.org/html/rfc3501#section-7) states that clients need to be able
+/// to accept any response at any time. These are the ones we've encountered in the wild.
+///
+/// Note that `Recent`, `Exists` and `Expunge` responses refer to the currently `SELECT`ed folder,
+/// so the user must take care when interpreting these.
+#[derive(Debug, PartialEq, Eq)]
+pub enum UnsolicitedResponse {
+    /// An unsolicited [`STATUS response`](https://tools.ietf.org/html/rfc3501#section-7.2.4).
+    Status {
+        /// The mailbox that this status response is for.
+        mailbox: String,
+        /// The attributes of this mailbox.
+        attributes: Vec<StatusAttribute>,
+    },
+
+    /// An unsolicited [`RECENT` response](https://tools.ietf.org/html/rfc3501#section-7.3.2)
+    /// indicating the number of messages with the `\Recent` flag set.  This response occurs if the
+    /// size of the mailbox changes (e.g., new messages arrive).
+    ///
+    /// > Note: It is not guaranteed that the message sequence
+    /// > numbers of recent messages will be a contiguous range of
+    /// > the highest n messages in the mailbox (where n is the
+    /// > value reported by the `RECENT` response).  Examples of
+    /// > situations in which this is not the case are: multiple
+    /// > clients having the same mailbox open (the first session
+    /// > to be notified will see it as recent, others will
+    /// > probably see it as non-recent), and when the mailbox is
+    /// > re-ordered by a non-IMAP agent.
+    /// >
+    /// > The only reliable way to identify recent messages is to
+    /// > look at message flags to see which have the `\Recent` flag
+    /// > set, or to do a `SEARCH RECENT`.
+    Recent(u32),
+
+    /// An unsolicited [`EXISTS` response](https://tools.ietf.org/html/rfc3501#section-7.3.1) that
+    /// reports the number of messages in the mailbox. This response occurs if the size of the
+    /// mailbox changes (e.g., new messages arrive).
+    Exists(u32),
+
+    /// An unsolicited [`EXPUNGE` response](https://tools.ietf.org/html/rfc3501#section-7.4.1) that
+    /// reports that the specified message sequence number has been permanently removed from the
+    /// mailbox.  The message sequence number for each successive message in the mailbox is
+    /// immediately decremented by 1, and this decrement is reflected in message sequence numbers
+    /// in subsequent responses (including other untagged `EXPUNGE` responses).
+    ///
+    /// The EXPUNGE response also decrements the number of messages in the mailbox; it is not
+    /// necessary to send an `EXISTS` response with the new value.
+    ///
+    /// As a result of the immediate decrement rule, message sequence numbers that appear in a set
+    /// of successive `EXPUNGE` responses depend upon whether the messages are removed starting
+    /// from lower numbers to higher numbers, or from higher numbers to lower numbers.  For
+    /// example, if the last 5 messages in a 9-message mailbox are expunged, a "lower to higher"
+    /// server will send five untagged `EXPUNGE` responses for message sequence number 5, whereas a
+    /// "higher to lower server" will send successive untagged `EXPUNGE` responses for message
+    /// sequence numbers 9, 8, 7, 6, and 5.
+    // TODO: the spec doesn't seem to say anything about when these may be received as unsolicited?
+    Expunge(u32),
+    /// Any other kind of unsolicted response.
+    Other(ResponseData),
+}

+ 62 - 0
async-imap-wasi/src/types/name.rs

@@ -0,0 +1,62 @@
+pub use imap_proto::types::NameAttribute;
+use imap_proto::{MailboxDatum, Response};
+use self_cell::self_cell;
+
+use crate::types::ResponseData;
+
+self_cell!(
+    /// A name that matches a `LIST` or `LSUB` command.
+    pub struct Name {
+        owner: Box<ResponseData>,
+
+        #[covariant]
+        dependent: InnerName,
+    }
+
+    impl { Debug }
+);
+
+#[derive(PartialEq, Eq, Debug)]
+pub struct InnerName<'a> {
+    attributes: Vec<NameAttribute<'a>>,
+    delimiter: Option<&'a str>,
+    name: &'a str,
+}
+
+impl Name {
+    pub(crate) fn from_mailbox_data(resp: ResponseData) -> Self {
+        Name::new(Box::new(resp), |response| match response.parsed() {
+            Response::MailboxData(MailboxDatum::List {
+                name_attributes,
+                delimiter,
+                name,
+            }) => InnerName {
+                attributes: name_attributes.to_owned(),
+                delimiter: delimiter.as_deref(),
+                name,
+            },
+            _ => panic!("cannot construct from non mailbox data"),
+        })
+    }
+
+    /// Attributes of this name.
+    pub fn attributes(&self) -> &[NameAttribute<'_>] {
+        &self.borrow_dependent().attributes[..]
+    }
+
+    /// The hierarchy delimiter is a character used to delimit levels of hierarchy in a mailbox
+    /// name.  A client can use it to create child mailboxes, and to search higher or lower levels
+    /// of naming hierarchy.  All children of a top-level hierarchy node use the same
+    /// separator character.  `None` means that no hierarchy exists; the name is a "flat" name.
+    pub fn delimiter(&self) -> Option<&str> {
+        self.borrow_dependent().delimiter
+    }
+
+    /// The name represents an unambiguous left-to-right hierarchy, and are valid for use as a
+    /// reference in `LIST` and `LSUB` commands. Unless [`NameAttribute::NoSelect`] is indicated,
+    /// the name is also valid as an argument for commands, such as `SELECT`, that accept mailbox
+    /// names.
+    pub fn name(&self) -> &str {
+        self.borrow_dependent().name
+    }
+}

+ 93 - 0
async-imap-wasi/src/types/quota.rs

@@ -0,0 +1,93 @@
+use imap_proto::types::Quota as QuotaRef;
+use imap_proto::types::QuotaResource as QuotaResourceRef;
+use imap_proto::types::QuotaResourceName as QuotaResourceNameRef;
+use imap_proto::types::QuotaRoot as QuotaRootRef;
+
+/// <https://tools.ietf.org/html/rfc2087#section-3>
+#[derive(Debug, Eq, PartialEq, Hash, Clone)]
+pub enum QuotaResourceName {
+    /// Sum of messages' RFC822.SIZE, in units of 1024 octets
+    Storage,
+    /// Number of messages
+    Message,
+    /// A different/custom resource
+    Atom(String),
+}
+
+impl<'a> From<QuotaResourceNameRef<'a>> for QuotaResourceName {
+    fn from(name: QuotaResourceNameRef<'_>) -> Self {
+        match name {
+            QuotaResourceNameRef::Message => QuotaResourceName::Message,
+            QuotaResourceNameRef::Storage => QuotaResourceName::Storage,
+            QuotaResourceNameRef::Atom(v) => QuotaResourceName::Atom(v.to_string()),
+        }
+    }
+}
+
+/// 5.1. QUOTA Response (<https://tools.ietf.org/html/rfc2087#section-5.1>)
+#[derive(Debug, Eq, PartialEq, Hash, Clone)]
+pub struct QuotaResource {
+    /// name of the resource
+    pub name: QuotaResourceName,
+    /// current usage of the resource
+    pub usage: u64,
+    /// resource limit
+    pub limit: u64,
+}
+
+impl<'a> From<QuotaResourceRef<'a>> for QuotaResource {
+    fn from(resource: QuotaResourceRef<'_>) -> Self {
+        Self {
+            name: resource.name.into(),
+            usage: resource.usage,
+            limit: resource.limit,
+        }
+    }
+}
+
+impl QuotaResource {
+    /// Returns the usage percentage of a QuotaResource.
+    pub fn get_usage_percentage(&self) -> u64 {
+        self.usage.saturating_mul(100) / self.limit
+    }
+}
+
+/// 5.1. QUOTA Response (<https://tools.ietf.org/html/rfc2087#section-5.1>)
+#[derive(Debug, Eq, PartialEq, Hash, Clone)]
+pub struct Quota {
+    /// quota root name
+    pub root_name: String,
+    /// quota resources for this quota
+    pub resources: Vec<QuotaResource>,
+}
+
+impl<'a> From<QuotaRef<'a>> for Quota {
+    fn from(quota: QuotaRef<'_>) -> Self {
+        Self {
+            root_name: quota.root_name.to_string(),
+            resources: quota.resources.iter().map(|r| r.clone().into()).collect(),
+        }
+    }
+}
+
+/// 5.2. QUOTAROOT Response (<https://tools.ietf.org/html/rfc2087#section-5.2>)
+#[derive(Debug, Eq, PartialEq, Hash, Clone)]
+pub struct QuotaRoot {
+    /// mailbox name
+    pub mailbox_name: String,
+    /// zero or more quota root names
+    pub quota_root_names: Vec<String>,
+}
+
+impl<'a> From<QuotaRootRef<'a>> for QuotaRoot {
+    fn from(root: QuotaRootRef<'_>) -> Self {
+        Self {
+            mailbox_name: root.mailbox_name.to_string(),
+            quota_root_names: root
+                .quota_root_names
+                .iter()
+                .map(|n| n.to_string())
+                .collect(),
+        }
+    }
+}

+ 4 - 0
async-imap-wasi/src/types/request.rs

@@ -0,0 +1,4 @@
+use imap_proto::RequestId;
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Request(pub Option<RequestId>, pub Vec<u8>);

+ 44 - 0
async-imap-wasi/src/types/response_data.rs

@@ -0,0 +1,44 @@
+use std::fmt;
+
+use bytes::BytesMut;
+use imap_proto::{RequestId, Response};
+use self_cell::self_cell;
+
+self_cell!(
+    pub struct ResponseData {
+        owner: BytesMut,
+
+        #[covariant]
+        dependent: Response,
+    }
+);
+
+impl std::cmp::PartialEq for ResponseData {
+    fn eq(&self, other: &Self) -> bool {
+        self.parsed() == other.parsed()
+    }
+}
+
+impl std::cmp::Eq for ResponseData {}
+
+impl fmt::Debug for ResponseData {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("ResponseData")
+            .field("raw", &self.borrow_owner().len())
+            .field("response", self.borrow_dependent())
+            .finish()
+    }
+}
+
+impl ResponseData {
+    pub fn request_id(&self) -> Option<&RequestId> {
+        match self.borrow_dependent() {
+            Response::Done { ref tag, .. } => Some(tag),
+            _ => None,
+        }
+    }
+
+    pub fn parsed(&self) -> &Response<'_> {
+        self.borrow_dependent()
+    }
+}