pax_global_header00006660000000000000000000000064147404757550014534gustar00rootroot0000000000000052 comment=5e26ef596abd232f4f42be1c205531c0af138848 imap-client-5e26ef596abd232f4f42be1c205531c0af138848/000077500000000000000000000000001474047575500206515ustar00rootroot00000000000000imap-client-5e26ef596abd232f4f42be1c205531c0af138848/.gitignore000066400000000000000000000001401474047575500226340ustar00rootroot00000000000000/target # IntelliJ, CLion, RustRover, ... .idea # direnv (https://direnv.net/) .envrc .direnv imap-client-5e26ef596abd232f4f42be1c205531c0af138848/CHANGELOG.md000066400000000000000000000012361474047575500224640ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.2.3] - 2025-01-11 ### Added - Added `trace` log of enqueued tasks. [himalaya#518] ## [0.2.2] - 2024-12-09 Starting from this release, we maintain the git history and the changelogs as clean as possible. The documentation will come in the nearest releases. [0.2.2]: https://github.com/pimalaya/imap-client/releases/tag/v0.2.2 [himalaya#518]: https://github.com/pimalaya/himalaya/issues/518imap-client-5e26ef596abd232f4f42be1c205531c0af138848/Cargo.lock000066400000000000000000001123311474047575500225570ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "abnf-core" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec182d1f071b906a9f59269c89af101515a5cbe58f723eb6717e7fe7445c0dea" dependencies = [ "nom", ] [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "async-attributes" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" dependencies = [ "quote", "syn 1.0.109", ] [[package]] name = "async-channel" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "slab", ] [[package]] name = "async-global-executor" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", "async-io", "async-lock", "blocking", "futures-lite", "once_cell", ] [[package]] name = "async-io" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "tracing", "windows-sys 0.59.0", ] [[package]] name = "async-lock" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ "event-listener 5.4.0", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-std" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" dependencies = [ "async-attributes", "async-channel 1.9.0", "async-global-executor", "async-io", "async-lock", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", "futures-lite", "gloo-timers", "kv-log-macro", "log", "memchr", "once_cell", "pin-project-lite", "pin-utils", "slab", "wasm-bindgen-futures", ] [[package]] name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" [[package]] name = "blocking" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", "futures-lite", "piper", ] [[package]] name = "bounded-static" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0beb903daa49b43bcafb5d5eebe633f9ad638d8b16cd08f95fb05ee7bd099321" [[package]] name = "bounded-static-derive" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0af050e27e5d57aa14975f97fe47a134c46a390f91819f23a625319a7111bfa" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a" dependencies = [ "shlex", ] [[package]] name = "cesu8" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "num-traits", ] [[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", ] [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener 5.4.0", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gloo-timers" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", "js-sys", "wasm-bindgen", ] [[package]] name = "hermit-abi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "imap-client" version = "0.2.3" dependencies = [ "async-std", "imap-client", "imap-next", "rip-starttls", "rustls-platform-verifier", "static_assertions", "thiserror 2.0.11", "tokio", "tokio-native-tls", "tokio-rustls", "tracing", ] [[package]] name = "imap-codec" version = "2.0.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f584310addd1fb8fe288e4f07c279fec9264ac1ea68b018241ae4dcd4fb28557" dependencies = [ "abnf-core", "base64", "chrono", "imap-types", "log", "nom", ] [[package]] name = "imap-next" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d85520e742d9e8d9edbf9df9e0876f560ed08650db8f9de562bc7cd46b9b43" dependencies = [ "bytes", "imap-codec", "thiserror 2.0.11", "tokio", "tokio-rustls", "tracing", ] [[package]] name = "imap-types" version = "2.0.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d601d81f11962a649acc2d535ad7311770e30364b4a978a762de291829c9ef53" dependencies = [ "base64", "bounded-static", "bounded-static-derive", "chrono", "rand", "thiserror 1.0.69", ] [[package]] name = "jni" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" dependencies = [ "cesu8", "combine", "jni-sys", "log", "thiserror 1.0.69", "walkdir", ] [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "kv-log-macro" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" dependencies = [ "log", ] [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d" dependencies = [ "value-bag", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi", "windows-sys 0.52.0", ] [[package]] name = "native-tls" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", "openssl-src", "pkg-config", "vcpkg", ] [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "pkg-config" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix", "tracing", "windows-sys 0.59.0", ] [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags", ] [[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rip-starttls" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edf1f62c7965137bde00221c63e20e511bb4d576e2ddf9bdf57e1d03042ba112" dependencies = [ "tokio", "tracing", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "rustls" version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", ] [[package]] name = "rustls-pemfile" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ "rustls-pki-types", ] [[package]] name = "rustls-pki-types" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-platform-verifier" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c7dc240fec5517e6c4eab3310438636cfe6391dfc345ba013109909a90d136" dependencies = [ "core-foundation", "core-foundation-sys", "jni", "log", "once_cell", "rustls", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.52.0", ] [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "num-bigint", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "thiserror-impl" version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "tokio" version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls", "tokio", ] [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "value-bag" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn 2.0.96", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-root-certs" version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd5da49bdf1f30054cfe0b8ce2958b8fbeb67c4d82c8967a598af481bef255c" dependencies = [ "rustls-pki-types", ] [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn 2.0.96", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" imap-client-5e26ef596abd232f4f42be1c205531c0af138848/Cargo.toml000066400000000000000000000026451474047575500226100ustar00rootroot00000000000000[package] name = "imap-client" description = "Rust library to manage IMAP clients" version = "0.2.3" authors = ["soywod "] edition = "2021" license = "MIT" categories = ["email"] keywords = ["email", "imap", "client"] homepage = "https://pimalaya.org/" documentation = "https://github.com/pimalaya/imap-client/" repository = "https://github.com/pimalaya/imap-client/" [features] default = [] # TLS providers # tokio-rustls = ["dep:tokio-rustls", "dep:rip-starttls", "dep:rustls-platform-verifier"] tokio-native-tls = ["dep:tokio-native-tls", "dep:rip-starttls"] # Vendored (mostly for OpenSSL) # vendored = ["tokio-native-tls?/vendored"] [dev-dependencies] async-std = { version = "1.13", features = ["attributes"] } imap-client = { path = ".", features = ["tokio-rustls", "tokio-native-tls"] } static_assertions = "1.1" tokio = { version = "1.37", features = ["full"] } [dependencies] imap-next = { version = "0.3", features = ["tag_generator", "ext_id", "ext_metadata"] } rip-starttls = { version = "0.1", optional = true, features = ["tokio"] } rustls-platform-verifier = { version = "0.4", optional = true } thiserror = "2" tokio = { version = "1.37", default-features = false, features = ["io-util", "net", "time"] } tokio-native-tls = { version = "0.3", optional = true } tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["logging", "tls12", "ring"] } tracing = "0.1" imap-client-5e26ef596abd232f4f42be1c205531c0af138848/LICENSE000066400000000000000000000021021474047575500216510ustar00rootroot00000000000000MIT License Copyright (c) 2024 soywod Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. imap-client-5e26ef596abd232f4f42be1c205531c0af138848/README.md000066400000000000000000000000421474047575500221240ustar00rootroot00000000000000# imap-client *Work In Progress* imap-client-5e26ef596abd232f4f42be1c205531c0af138848/deny.toml000066400000000000000000000005501474047575500225050ustar00rootroot00000000000000[sources] unknown-registry = "deny" unknown-git = "deny" allow-git = [ "https://github.com/duesee/imap-codec", ] [licenses] allow = [ "Apache-2.0", "BSD-3-Clause", "MIT", "Unicode-DFS-2016", "ISC", "OpenSSL" ] [[licenses.clarify]] name = "ring" expression = "MIT AND ISC AND OpenSSL" license-files = [ { path = "LICENSE", hash = 0xbd0eed23 } ] imap-client-5e26ef596abd232f4f42be1c205531c0af138848/examples/000077500000000000000000000000001474047575500224675ustar00rootroot00000000000000imap-client-5e26ef596abd232f4f42be1c205531c0af138848/examples/fetch.rs000066400000000000000000000027151474047575500241330ustar00rootroot00000000000000use imap_client::{ imap_types::{ fetch::{Macro, MacroOrMessageDataItemNames, MessageDataItem}, sequence::SequenceSet, }, Client, }; const USAGE: &str = "USAGE: cargo run --example=fetch -- "; #[tokio::main] async fn main() { let (host, port, username, password) = { let mut args = std::env::args(); let _ = args.next(); ( args.next().expect(USAGE), str::parse::(&args.next().expect(USAGE)).unwrap(), args.next().expect(USAGE), args.next().expect(USAGE), ) }; let mut client = Client::tls(host, port).await.unwrap(); client.authenticate_plain(username, password).await.unwrap(); let select_data = client.select("inbox").await.unwrap(); println!("{select_data:?}\n"); let data = client .fetch( SequenceSet::try_from("1:10").unwrap(), MacroOrMessageDataItemNames::Macro(Macro::Full), ) .await .unwrap(); println!("# INBOX\n"); for (_, items) in data { let envelope = items .as_ref() .iter() .find(|item| matches!(item, MessageDataItem::Envelope(_))) .unwrap(); if let MessageDataItem::Envelope(env) = envelope { if let Some(sub) = &env.subject.0 { println!("* {:?}", std::str::from_utf8(sub.as_ref()).unwrap()); } } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/flake.lock000066400000000000000000000037551474047575500226170ustar00rootroot00000000000000{ "nodes": { "fenix": { "inputs": { "nixpkgs": [ "nixpkgs" ], "rust-analyzer-src": "rust-analyzer-src" }, "locked": { "lastModified": 1732405626, "narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=", "owner": "soywod", "repo": "fenix", "rev": "c7af381484169a78fb79a11652321ae80b0f92a6", "type": "github" }, "original": { "owner": "soywod", "repo": "fenix", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1732539489, "narHash": "sha256-EPstM4aUuClDkFdoyno1CSRZIfGdCfZKw/XzMlQB0NY=", "owner": "soywod", "repo": "nixpkgs", "rev": "87b010196489f96e4acdcc804ed3d096476e46ad", "type": "github" }, "original": { "owner": "soywod", "repo": "nixpkgs", "type": "github" } }, "pimalaya": { "flake": false, "locked": { "lastModified": 1732872950, "narHash": "sha256-h/2KHwd+9ECaoKT+V/G4KusJgsXtFVrqYkn4HS1NpUY=", "owner": "pimalaya", "repo": "nix", "rev": "6c807f38875787fe7ce7eae7fbb5f48eaaf490ec", "type": "github" }, "original": { "owner": "pimalaya", "repo": "nix", "type": "github" } }, "root": { "inputs": { "fenix": "fenix", "nixpkgs": "nixpkgs", "pimalaya": "pimalaya" } }, "rust-analyzer-src": { "flake": false, "locked": { "lastModified": 1732050317, "narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=", "owner": "rust-lang", "repo": "rust-analyzer", "rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4", "type": "github" }, "original": { "owner": "rust-lang", "ref": "nightly", "repo": "rust-analyzer", "type": "github" } } }, "root": "root", "version": 7 } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/flake.nix000066400000000000000000000011551474047575500224550ustar00rootroot00000000000000{ description = "Rust library to manage IMAP clients"; inputs = { # TODO: https://github.com/NixOS/nixpkgs/pull/358989 # nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:soywod/nixpkgs"; fenix = { # TODO: https://github.com/nix-community/fenix/pull/145 # url = "github:nix-community/fenix"; url = "github:soywod/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; pimalaya = { url = "github:pimalaya/nix"; flake = false; }; }; outputs = inputs: (import inputs.pimalaya).mkFlakeOutputs inputs { shell = ./shell.nix; }; } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/rust-toolchain.toml000066400000000000000000000001361474047575500245210ustar00rootroot00000000000000[toolchain] channel = "1.82.0" profile = "default" components = ["rust-src", "rust-analyzer"] imap-client-5e26ef596abd232f4f42be1c205531c0af138848/rustfmt.toml000066400000000000000000000001361474047575500232520ustar00rootroot00000000000000format_code_in_doc_comments=true group_imports="StdExternalCrate" imports_granularity="Crate" imap-client-5e26ef596abd232f4f42be1c205531c0af138848/shell.nix000066400000000000000000000003221474047575500224750ustar00rootroot00000000000000{ pimalaya ? import (fetchTarball "https://github.com/pimalaya/nix/archive/master.tar.gz") , ... } @args: pimalaya.mkShell ({ rustToolchainFile = ./rust-toolchain.toml; } // removeAttrs args [ "pimalaya" ]) imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/000077500000000000000000000000001474047575500214405ustar00rootroot00000000000000imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/client/000077500000000000000000000000001474047575500227165ustar00rootroot00000000000000imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/client/mod.rs000066400000000000000000000116471474047575500240540ustar00rootroot00000000000000pub mod tokio; use std::time::Duration; use imap_next::{ client::Options as ClientOptions, imap_types::{auth::AuthMechanism, core::Vec1, response::Capability}, }; use crate::tasks::resolver::Resolver; pub struct Client { resolver: Resolver, capabilities: Vec1>, idle_timeout: Duration, } impl Client { pub fn new(opts: ClientOptions) -> Self { let client = imap_next::client::Client::new(opts); let resolver = Resolver::new(client); Self { resolver, capabilities: Vec1::from(Capability::Imap4Rev1), idle_timeout: Duration::from_secs(5 * 60), // 5 min } } pub fn get_idle_timeout(&self) -> &Duration { &self.idle_timeout } pub fn set_idle_timeout(&mut self, timeout: Duration) { self.idle_timeout = timeout; } pub fn set_some_idle_timeout(&mut self, timeout: Option) { if let Some(timeout) = timeout { self.set_idle_timeout(timeout) } } pub fn with_idle_timeout(mut self, timeout: Duration) -> Self { self.set_idle_timeout(timeout); self } pub fn with_some_idle_timeout(mut self, timeout: Option) -> Self { self.set_some_idle_timeout(timeout); self } /// Returns the server capabilities. /// /// This function does not *fetch* capabilities from server, it /// just returns capabilities saved during the creation of this /// client (using [`Client::insecure`], [`Client::tls`] or /// [`Client::starttls`]). pub fn capabilities(&self) -> &Vec1> { &self.capabilities } /// Returns the server capabilities, as an iterator. /// /// Same as [`Client::capabilities`], but just returns an iterator /// instead. pub fn capabilities_iter(&self) -> impl Iterator> + '_ { self.capabilities().as_ref().iter() } /// Returns supported authentication mechanisms, as an iterator. pub fn supported_auth_mechanisms(&self) -> impl Iterator> + '_ { self.capabilities_iter().filter_map(|capability| { if let Capability::Auth(mechanism) = capability { Some(mechanism) } else { None } }) } /// Returns `true` if the given authentication mechanism is /// supported by the server. pub fn supports_auth_mechanism(&self, mechanism: AuthMechanism<'static>) -> bool { self.capabilities_iter().any(|capability| { if let Capability::Auth(m) = capability { m == &mechanism } else { false } }) } /// Returns `true` if `LOGIN` is supported by the server. pub fn login_supported(&self) -> bool { !self .capabilities_iter() .any(|c| matches!(c, Capability::LoginDisabled)) } /// Returns `true` if the `ENABLE` extension is supported by the /// server. pub fn ext_enable_supported(&self) -> bool { self.capabilities_iter() .any(|c| matches!(c, Capability::Enable)) } /// Returns `true` if the `SASL-IR` extension is supported by the /// server. pub fn ext_sasl_ir_supported(&self) -> bool { self.capabilities_iter() .any(|c| matches!(c, Capability::SaslIr)) } /// Returns `true` if the `ID` extension is supported by the /// server. pub fn ext_id_supported(&self) -> bool { self.capabilities_iter() .any(|c| matches!(c, Capability::Id)) } /// Returns `true` if the `UIDPLUS` extension is supported by the /// server. pub fn ext_uidplus_supported(&self) -> bool { self.capabilities_iter() .any(|c| matches!(c, Capability::UidPlus)) } /// Returns `true` if the `SORT` extension is supported by the /// server. pub fn ext_sort_supported(&self) -> bool { self.capabilities_iter() .any(|c| matches!(c, Capability::Sort(_))) } /// Returns `true` if the `THREAD` extension is supported by the /// server. pub fn ext_thread_supported(&self) -> bool { self.capabilities_iter() .any(|c| matches!(c, Capability::Thread(_))) } /// Returns `true` if the `IDLE` extension is supported by the /// server. pub fn ext_idle_supported(&self) -> bool { self.capabilities_iter() .any(|c| matches!(c, Capability::Idle)) } /// Returns `true` if the `BINARY` extension is supported by the /// server. pub fn ext_binary_supported(&self) -> bool { self.capabilities_iter() .any(|c| matches!(c, Capability::Binary)) } /// Returns `true` if the `MOVE` extension is supported by the /// server. pub fn ext_move_supported(&self) -> bool { self.capabilities_iter() .any(|c| matches!(c, Capability::Move)) } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/client/tokio.rs000066400000000000000000001326101474047575500244140ustar00rootroot00000000000000use std::{ cmp::Ordering, collections::HashMap, io, num::NonZeroU32, pin::Pin, task::{Context, Poll}, }; use imap_next::{ client::{Error as NextError, Event, Options as ClientOptions}, imap_types::{ command::{Command, CommandBody}, core::{AString, IString, Literal, LiteralMode, NString, QuotedChar, Tag, Vec1}, error::ValidationError, extensions::{ binary::{Literal8, LiteralOrLiteral8}, enable::CapabilityEnable, sort::{SortCriterion, SortKey}, thread::{Thread, ThreadingAlgorithm}, }, fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName}, flag::{Flag, FlagNameAttribute, StoreType}, mailbox::{ListMailbox, Mailbox}, response::{Code, Status, Tagged}, search::SearchKey, secret::Secret, sequence::SequenceSet, IntoStatic, }, }; #[cfg(any(feature = "tokio-rustls", feature = "tokio-native-tls"))] use rip_starttls::imap::tokio::RipStarttls; use thiserror::Error; use tokio::{ io::{AsyncRead, AsyncWrite, ReadBuf}, net::TcpStream, time::timeout, }; use tracing::{debug, trace, warn}; use crate::{ stream::{self, Stream}, tasks::{ tasks::{ append::{AppendTask, PostAppendCheckTask, PostAppendNoOpTask}, appenduid::AppendUidTask, authenticate::AuthenticateTask, capability::CapabilityTask, check::CheckTask, copy::CopyTask, create::CreateTask, delete::DeleteTask, enable::EnableTask, expunge::ExpungeTask, fetch::{FetchFirstTask, FetchTask}, id::IdTask, list::ListTask, login::LoginTask, noop::NoOpTask, r#move::MoveTask, search::SearchTask, select::{SelectDataUnvalidated, SelectTask}, sort::SortTask, store::StoreTask, thread::ThreadTask, TaskError, }, SchedulerError, SchedulerEvent, Task, }, }; static MAX_SEQUENCE_SIZE: u8 = u8::MAX; // 255 #[derive(Debug, Error)] pub enum ClientError { #[error("cannot upgrade client to TLS: client is already in TLS state")] ClientAlreadyTlsError, #[error("cannot do STARTTLS prefix")] DoStarttlsPrefixError(#[from] io::Error), #[error("stream error")] Stream(#[from] stream::Error), #[error("validation error")] Validation(#[from] ValidationError), #[error("cannot connect to TCP stream")] ConnectToTcpStreamError(#[source] io::Error), #[error("cannot connect to TLS stream")] ConnectToTlsStreamError(#[source] io::Error), #[cfg(feature = "tokio-native-tls")] #[error("cannot connect to native TLS stream")] ConnectToNativeTlsStreamError(#[source] tokio_native_tls::native_tls::Error), #[cfg(feature = "tokio-native-tls")] #[error("cannot create native TLS connector")] CreateNativeTlsConnectorError(#[source] tokio_native_tls::native_tls::Error), #[error("cannot receive greeting from server")] ReceiveGreeting(#[source] stream::Error), #[error("cannot resolve IMAP task")] ResolveTask(#[from] TaskError), } pub enum MaybeTlsStream { Plain(TcpStream), #[cfg(feature = "tokio-rustls")] Rustls(tokio_rustls::client::TlsStream), #[cfg(feature = "tokio-native-tls")] NativeTls(tokio_native_tls::TlsStream), } impl AsyncRead for MaybeTlsStream { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { match self.get_mut() { Self::Plain(s) => Pin::new(s).poll_read(cx, buf), #[cfg(feature = "tokio-rustls")] Self::Rustls(s) => Pin::new(s).poll_read(cx, buf), #[cfg(feature = "tokio-native-tls")] Self::NativeTls(s) => Pin::new(s).poll_read(cx, buf), } } } impl AsyncWrite for MaybeTlsStream { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { match self.get_mut() { Self::Plain(s) => Pin::new(s).poll_write(cx, buf), #[cfg(feature = "tokio-rustls")] Self::Rustls(s) => Pin::new(s).poll_write(cx, buf), #[cfg(feature = "tokio-native-tls")] Self::NativeTls(s) => Pin::new(s).poll_write(cx, buf), } } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.get_mut() { Self::Plain(s) => Pin::new(s).poll_flush(cx), #[cfg(feature = "tokio-rustls")] Self::Rustls(s) => Pin::new(s).poll_flush(cx), #[cfg(feature = "tokio-native-tls")] Self::NativeTls(s) => Pin::new(s).poll_flush(cx), } } fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.get_mut() { Self::Plain(s) => Pin::new(s).poll_shutdown(cx), #[cfg(feature = "tokio-rustls")] Self::Rustls(s) => Pin::new(s).poll_shutdown(cx), #[cfg(feature = "tokio-native-tls")] Self::NativeTls(s) => Pin::new(s).poll_shutdown(cx), } } } pub struct Client { host: String, pub state: super::Client, pub stream: Stream, } /// Client constructors. /// /// This section defines 3 public constructors for [`Client`]: /// `insecure`, `tls` and `starttls`. impl Client { /// Creates an insecure client, using TCP. /// /// This constructor creates a client based on an raw /// [`TcpStream`], receives greeting then saves server /// capabilities. pub async fn insecure(host: impl ToString, port: u16) -> Result { let mut client = Self::tcp(host, port, false).await?; if !client.receive_greeting().await? { client.refresh_capabilities().await?; } Ok(client) } /// Creates a secure client, using SSL/TLS or STARTTLS. /// /// This constructor creates an client based on a secure /// [`TcpStream`] wrapped into a [`TlsStream`], receives greeting /// then saves server capabilities. #[cfg(feature = "tokio-rustls")] pub async fn rustls( host: impl ToString, port: u16, starttls: bool, ) -> Result { let tcp = Self::tcp(host, port, starttls).await?; Self::upgrade_rustls(tcp, starttls).await } /// Creates a secure client, using SSL/TLS or STARTTLS. /// /// This constructor creates an client based on a secure /// [`TcpStream`] wrapped into a [`TlsStream`], receives greeting /// then saves server capabilities. #[cfg(feature = "tokio-native-tls")] pub async fn native_tls( host: impl ToString, port: u16, starttls: bool, ) -> Result { let tcp = Self::tcp(host, port, starttls).await?; Self::upgrade_native_tls(tcp, starttls).await } /// Creates an insecure client based on a raw [`TcpStream`]. /// /// This function is internally used by public constructors /// `insecure`, `tls` and `starttls`. async fn tcp( host: impl ToString, port: u16, discard_greeting: bool, ) -> Result { let host = host.to_string(); let tcp_stream = TcpStream::connect((host.as_str(), port)) .await .map_err(ClientError::ConnectToTcpStreamError)?; let stream = Stream::new(MaybeTlsStream::Plain(tcp_stream)); let mut opts = ClientOptions::default(); opts.crlf_relaxed = true; opts.discard_greeting = discard_greeting; let state = super::Client::new(opts); Ok(Self { host, stream, state, }) } /// Turns an insecure client into a secure one. /// /// The flow changes depending on the `starttls` parameter: /// /// If `true`: receives greeting, sends STARTTLS command, upgrades /// to TLS then force-refreshes server capabilities. /// /// If `false`: upgrades straight to TLS, receives greeting then /// refreshes server capabilities if needed. #[cfg(feature = "tokio-rustls")] async fn upgrade_rustls(mut self, starttls: bool) -> Result { use std::sync::Arc; use rustls_platform_verifier::ConfigVerifierExt; use tokio_rustls::{ rustls::{pki_types::ServerName, ClientConfig}, TlsConnector, }; let MaybeTlsStream::Plain(mut tcp_stream) = self.stream.into_inner() else { return Err(ClientError::ClientAlreadyTlsError); }; if starttls { tcp_stream = RipStarttls::default() .do_starttls_prefix(tcp_stream) .await .map_err(ClientError::DoStarttlsPrefixError)?; } let mut config = ClientConfig::with_platform_verifier(); // See config.alpn_protocols = vec![b"imap".to_vec()]; let connector = TlsConnector::from(Arc::new(config)); let dnsname = ServerName::try_from(self.host.clone()).unwrap(); let tls_stream = connector .connect(dnsname, tcp_stream) .await .map_err(ClientError::ConnectToTlsStreamError)?; self.stream = Stream::new(MaybeTlsStream::Rustls(tls_stream)); if starttls || !self.receive_greeting().await? { self.refresh_capabilities().await?; } Ok(self) } /// Turns an insecure client into a secure one. /// /// The flow changes depending on the `starttls` parameter: /// /// If `true`: receives greeting, sends STARTTLS command, upgrades /// to TLS then force-refreshes server capabilities. /// /// If `false`: upgrades straight to TLS, receives greeting then /// refreshes server capabilities if needed. #[cfg(feature = "tokio-native-tls")] async fn upgrade_native_tls(mut self, starttls: bool) -> Result { use tokio_native_tls::{native_tls, TlsConnector}; let MaybeTlsStream::Plain(mut tcp_stream) = self.stream.into_inner() else { return Err(ClientError::ClientAlreadyTlsError); }; if starttls { tcp_stream = RipStarttls::default() .do_starttls_prefix(tcp_stream) .await .map_err(ClientError::DoStarttlsPrefixError)?; } let connector = native_tls::TlsConnector::new().map_err(ClientError::CreateNativeTlsConnectorError)?; let connector = TlsConnector::from(connector); let tls_stream = connector .connect(&self.host, tcp_stream) .await .map_err(ClientError::ConnectToNativeTlsStreamError)?; self.stream = Stream::new(MaybeTlsStream::NativeTls(tls_stream)); if starttls || !self.receive_greeting().await? { self.refresh_capabilities().await?; } Ok(self) } /// Receives server greeting. /// /// Returns `true` if server capabilities were found in the /// greeting, otherwise `false`. This boolean is internally used /// to determine if server capabilities need to be explicitly /// requested or not. async fn receive_greeting(&mut self) -> Result { let evt = self .stream .next(&mut self.state.resolver) .await .map_err(ClientError::ReceiveGreeting)?; if let SchedulerEvent::GreetingReceived(greeting) = evt { if let Some(Code::Capability(capabilities)) = greeting.code { self.state.capabilities = capabilities; return Ok(true); } } Ok(false) } } /// Client low-level API. /// /// This section defines the low-level API of the client, by exposing /// convenient wrappers around [`Task`]s. They do not contain any /// logic. impl Client { /// Resolves the given [`Task`]. pub async fn resolve(&mut self, task: T) -> Result { Ok(self.stream.next(self.state.resolver.resolve(task)).await?) } /// Enables the given capabilities. pub async fn enable( &mut self, capabilities: impl IntoIterator>, ) -> Result>>, ClientError> { if !self.state.ext_enable_supported() { warn!("IMAP ENABLE extension not supported, skipping"); return Ok(None); } let capabilities: Vec<_> = capabilities .into_iter() .map(IntoStatic::into_static) .collect(); if capabilities.is_empty() { return Ok(None); } let capabilities = Vec1::try_from(capabilities).unwrap(); Ok(self.resolve(EnableTask::new(capabilities)).await??) } /// Creates a new mailbox. pub async fn create( &mut self, mailbox: impl TryInto, Error = ValidationError>, ) -> Result<(), ClientError> { let mbox = mailbox.try_into()?.into_static(); Ok(self.resolve(CreateTask::new(mbox)).await??) } /// Lists mailboxes. pub async fn list( &mut self, mailbox: impl TryInto, Error = ValidationError>, mailbox_wildcard: impl TryInto, Error = ValidationError>, ) -> Result< Vec<( Mailbox<'static>, Option, Vec>, )>, ClientError, > { let mbox = mailbox.try_into()?.into_static(); let mbox_wcard = mailbox_wildcard.try_into()?.into_static(); Ok(self.resolve(ListTask::new(mbox, mbox_wcard)).await??) } /// Selects the given mailbox. pub async fn select( &mut self, mailbox: impl TryInto, Error = ValidationError>, ) -> Result { let mbox = mailbox.try_into()?.into_static(); Ok(self.resolve(SelectTask::new(mbox)).await??) } /// Selects the given mailbox in read-only mode. pub async fn examine( &mut self, mailbox: impl TryInto, Error = ValidationError>, ) -> Result { let mbox = mailbox.try_into()?.into_static(); Ok(self.resolve(SelectTask::read_only(mbox)).await??) } /// Expunges the selected mailbox. /// /// A mailbox needs to be selected before, otherwise this function /// will fail. pub async fn expunge(&mut self) -> Result, ClientError> { Ok(self.resolve(ExpungeTask::new()).await??) } /// Deletes the given mailbox. pub async fn delete( &mut self, mailbox: impl TryInto, Error = ValidationError>, ) -> Result<(), ClientError> { let mbox = mailbox.try_into()?.into_static(); Ok(self.resolve(DeleteTask::new(mbox)).await??) } /// Searches messages matching the given criteria. async fn _search( &mut self, criteria: impl IntoIterator>, uid: bool, ) -> Result, ClientError> { let criteria: Vec<_> = criteria.into_iter().map(IntoStatic::into_static).collect(); let criteria = if criteria.is_empty() { Vec1::from(SearchKey::All) } else { Vec1::try_from(criteria).unwrap() }; Ok(self .resolve(SearchTask::new(criteria).with_uid(uid)) .await??) } /// Searches messages matching the given criteria. /// /// This function returns sequence numbers, if you need UID see /// [`Client::uid_search`]. pub async fn search( &mut self, criteria: impl IntoIterator>, ) -> Result, ClientError> { self._search(criteria, false).await } /// Searches messages matching the given criteria. /// /// This function returns UIDs, if you need sequence numbers see /// [`Client::search`]. pub async fn uid_search( &mut self, criteria: impl IntoIterator>, ) -> Result, ClientError> { self._search(criteria, true).await } /// Searches messages matching the given search criteria, sorted /// by the given sort criteria. async fn _sort( &mut self, sort_criteria: impl IntoIterator, search_criteria: impl IntoIterator>, uid: bool, ) -> Result, ClientError> { let sort: Vec<_> = sort_criteria.into_iter().collect(); let sort = if sort.is_empty() { Vec1::from(SortCriterion { reverse: true, key: SortKey::Date, }) } else { Vec1::try_from(sort).unwrap() }; let search: Vec<_> = search_criteria .into_iter() .map(IntoStatic::into_static) .collect(); let search = if search.is_empty() { Vec1::from(SearchKey::All) } else { Vec1::try_from(search).unwrap() }; Ok(self .resolve(SortTask::new(sort, search).with_uid(uid)) .await??) } /// Searches messages matching the given search criteria, sorted /// by the given sort criteria. /// /// This function returns sequence numbers, if you need UID see /// [`Client::uid_sort`]. pub async fn sort( &mut self, sort_criteria: impl IntoIterator, search_criteria: impl IntoIterator>, ) -> Result, ClientError> { self._sort(sort_criteria, search_criteria, false).await } /// Searches messages matching the given search criteria, sorted /// by the given sort criteria. /// /// This function returns UIDs, if you need sequence numbers see /// [`Client::sort`]. pub async fn uid_sort( &mut self, sort_criteria: impl IntoIterator, search_criteria: impl IntoIterator>, ) -> Result, ClientError> { self._sort(sort_criteria, search_criteria, true).await } async fn _thread( &mut self, algorithm: ThreadingAlgorithm<'_>, search_criteria: impl IntoIterator>, uid: bool, ) -> Result, ClientError> { let alg = algorithm.into_static(); let search: Vec<_> = search_criteria .into_iter() .map(IntoStatic::into_static) .collect(); let search = if search.is_empty() { Vec1::from(SearchKey::All) } else { Vec1::try_from(search).unwrap() }; Ok(self .resolve(ThreadTask::new(alg, search).with_uid(uid)) .await??) } pub async fn thread( &mut self, algorithm: ThreadingAlgorithm<'_>, search_criteria: impl IntoIterator>, ) -> Result, ClientError> { self._thread(algorithm, search_criteria, false).await } pub async fn uid_thread( &mut self, algorithm: ThreadingAlgorithm<'_>, search_criteria: impl IntoIterator>, ) -> Result, ClientError> { self._thread(algorithm, search_criteria, true).await } async fn _store( &mut self, sequence_set: SequenceSet, kind: StoreType, flags: impl IntoIterator>, uid: bool, ) -> Result>>, ClientError> { let flags: Vec<_> = flags.into_iter().map(IntoStatic::into_static).collect(); Ok(self .resolve(StoreTask::new(sequence_set, kind, flags).with_uid(uid)) .await??) } pub async fn store( &mut self, sequence_set: SequenceSet, kind: StoreType, flags: impl IntoIterator>, ) -> Result>>, ClientError> { self._store(sequence_set, kind, flags, false).await } pub async fn uid_store( &mut self, sequence_set: SequenceSet, kind: StoreType, flags: impl IntoIterator>, ) -> Result>>, ClientError> { self._store(sequence_set, kind, flags, true).await } async fn _silent_store( &mut self, sequence_set: SequenceSet, kind: StoreType, flags: impl IntoIterator>, uid: bool, ) -> Result<(), ClientError> { let flags: Vec<_> = flags.into_iter().map(IntoStatic::into_static).collect(); let task = StoreTask::new(sequence_set, kind, flags) .with_uid(uid) .silent(); Ok(self.resolve(task).await??) } pub async fn silent_store( &mut self, sequence_set: SequenceSet, kind: StoreType, flags: impl IntoIterator>, ) -> Result<(), ClientError> { self._silent_store(sequence_set, kind, flags, false).await } pub async fn uid_silent_store( &mut self, sequence_set: SequenceSet, kind: StoreType, flags: impl IntoIterator>, ) -> Result<(), ClientError> { self._silent_store(sequence_set, kind, flags, true).await } pub async fn post_append_noop(&mut self) -> Result, ClientError> { Ok(self.resolve(PostAppendNoOpTask::new()).await??) } pub async fn post_append_check(&mut self) -> Result, ClientError> { Ok(self.resolve(PostAppendCheckTask::new()).await??) } async fn _fetch_first( &mut self, id: NonZeroU32, items: MacroOrMessageDataItemNames<'_>, uid: bool, ) -> Result>, ClientError> { let items = items.into_static(); Ok(self .resolve(FetchFirstTask::new(id, items).with_uid(uid)) .await??) } pub async fn fetch_first( &mut self, id: NonZeroU32, items: MacroOrMessageDataItemNames<'_>, ) -> Result>, ClientError> { self._fetch_first(id, items, false).await } pub async fn uid_fetch_first( &mut self, id: NonZeroU32, items: MacroOrMessageDataItemNames<'_>, ) -> Result>, ClientError> { self._fetch_first(id, items, true).await } async fn _copy( &mut self, sequence_set: SequenceSet, mailbox: impl TryInto, Error = ValidationError>, uid: bool, ) -> Result<(), ClientError> { let mbox = mailbox.try_into()?.into_static(); Ok(self .resolve(CopyTask::new(sequence_set, mbox).with_uid(uid)) .await??) } pub async fn copy( &mut self, sequence_set: SequenceSet, mailbox: impl TryInto, Error = ValidationError>, ) -> Result<(), ClientError> { self._copy(sequence_set, mailbox, false).await } pub async fn uid_copy( &mut self, sequence_set: SequenceSet, mailbox: impl TryInto, Error = ValidationError>, ) -> Result<(), ClientError> { self._copy(sequence_set, mailbox, true).await } async fn _move( &mut self, sequence_set: SequenceSet, mailbox: impl TryInto, Error = ValidationError>, uid: bool, ) -> Result<(), ClientError> { let mbox = mailbox.try_into()?.into_static(); Ok(self .resolve(MoveTask::new(sequence_set, mbox).with_uid(uid)) .await??) } pub async fn r#move( &mut self, sequence_set: SequenceSet, mailbox: impl TryInto, Error = ValidationError>, ) -> Result<(), ClientError> { self._move(sequence_set, mailbox, false).await } pub async fn uid_move( &mut self, sequence_set: SequenceSet, mailbox: impl TryInto, Error = ValidationError>, ) -> Result<(), ClientError> { self._move(sequence_set, mailbox, true).await } /// Executes the `CHECK` command. pub async fn check(&mut self) -> Result<(), ClientError> { Ok(self.resolve(CheckTask::new()).await??) } /// Executes the `NOOP` command. pub async fn noop(&mut self) -> Result<(), ClientError> { Ok(self.resolve(NoOpTask::new()).await??) } } /// Client medium-level API. /// /// This section defines the medium-level API of the client (based on /// the low-level one), by exposing helpers that update client state /// and use a small amount of logic (mostly conditional code depending /// on available server capabilities). impl Client { /// Fetches server capabilities, then saves them. pub async fn refresh_capabilities(&mut self) -> Result<(), ClientError> { self.state.capabilities = self.resolve(CapabilityTask::new()).await??; Ok(()) } /// Identifies the user using the given username and password. pub async fn login( &mut self, username: impl TryInto, Error = ValidationError>, password: impl TryInto, Error = ValidationError>, ) -> Result<(), ClientError> { let username = username.try_into()?.into_static(); let password = password.try_into()?.into_static(); let login = self.resolve(LoginTask::new(username, Secret::new(password))); match login.await?? { Some(capabilities) => { self.state.capabilities = capabilities; } None => { self.refresh_capabilities().await?; } }; Ok(()) } /// Authenticates the user using the given [`AuthenticateTask`]. /// /// This function also refreshes capabilities (either from the /// task output or from explicit request). async fn authenticate(&mut self, task: AuthenticateTask) -> Result<(), ClientError> { match self.resolve(task).await?? { Some(capabilities) => { self.state.capabilities = capabilities; } None => { self.refresh_capabilities().await?; } }; Ok(()) } /// Authenticates the user using the `PLAIN` mechanism. pub async fn authenticate_plain( &mut self, login: impl AsRef, password: impl AsRef, ) -> Result<(), ClientError> { self.authenticate(AuthenticateTask::plain( login.as_ref(), password.as_ref(), self.state.ext_sasl_ir_supported(), )) .await } /// Authenticates the user using the `XOAUTH2` mechanism. pub async fn authenticate_xoauth2( &mut self, login: impl AsRef, token: impl AsRef, ) -> Result<(), ClientError> { self.authenticate(AuthenticateTask::xoauth2( login.as_ref(), token.as_ref(), self.state.ext_sasl_ir_supported(), )) .await } /// Authenticates the user using the `OAUTHBEARER` mechanism. pub async fn authenticate_oauthbearer( &mut self, user: impl AsRef, host: impl AsRef, port: u16, token: impl AsRef, ) -> Result<(), ClientError> { self.authenticate(AuthenticateTask::oauthbearer( user.as_ref(), host.as_ref(), port, token.as_ref(), self.state.ext_sasl_ir_supported(), )) .await } /// Exchanges client/server ids. /// /// If the server does not support the `ID` extension, this /// function has no effect. pub async fn id( &mut self, params: Option, NString<'static>)>>, ) -> Result, NString<'static>)>>, ClientError> { Ok(if self.state.ext_id_supported() { self.resolve(IdTask::new(params)).await?? } else { warn!("IMAP ID extension not supported, skipping"); None }) } pub async fn append( &mut self, mailbox: impl TryInto, Error = ValidationError>, flags: impl IntoIterator>, message: impl AsRef<[u8]>, ) -> Result, ClientError> { let mbox = mailbox.try_into()?.into_static(); let flags: Vec<_> = flags.into_iter().map(IntoStatic::into_static).collect(); let msg = to_static_literal(message, self.state.ext_binary_supported())?; Ok(self .resolve(AppendTask::new(mbox, msg).with_flags(flags)) .await??) } pub async fn appenduid( &mut self, mailbox: impl TryInto, Error = ValidationError>, flags: impl IntoIterator>, message: impl AsRef<[u8]>, ) -> Result, ClientError> { let mbox = mailbox.try_into()?.into_static(); let flags: Vec<_> = flags.into_iter().map(IntoStatic::into_static).collect(); let msg = to_static_literal(message, self.state.ext_binary_supported())?; Ok(self .resolve(AppendUidTask::new(mbox, msg).with_flags(flags)) .await??) } } /// Client high-level API. /// /// This section defines the high-level API of the client (based on /// the low and medium ones), by exposing opinionated helpers. They /// contain more logic, and make use of fallbacks depending on /// available server capabilities. impl Client { async fn _fetch( &mut self, sequence_set: SequenceSet, items: MacroOrMessageDataItemNames<'_>, uid: bool, ) -> Result>>, ClientError> { let mut items = match items { MacroOrMessageDataItemNames::Macro(m) => m.expand().into_static(), MacroOrMessageDataItemNames::MessageDataItemNames(items) => items.into_static(), }; if uid { items.push(MessageDataItemName::Uid); } let seq_map = self .resolve(FetchTask::new(sequence_set, items.into()).with_uid(uid)) .await??; if uid { let mut uid_map = HashMap::new(); for (seq, items) in seq_map { let uid = items.as_ref().iter().find_map(|item| { if let MessageDataItem::Uid(uid) = item { Some(*uid) } else { None } }); match uid { Some(uid) => { uid_map.insert(uid, items); } None => { warn!(?seq, "cannot get message uid, skipping it"); } } } Ok(uid_map) } else { Ok(seq_map) } } pub async fn fetch( &mut self, sequence_set: SequenceSet, items: MacroOrMessageDataItemNames<'_>, ) -> Result>>, ClientError> { self._fetch(sequence_set, items, false).await } pub async fn uid_fetch( &mut self, sequence_set: SequenceSet, items: MacroOrMessageDataItemNames<'_>, ) -> Result>>, ClientError> { self._fetch(sequence_set, items, true).await } async fn _sort_or_fallback( &mut self, sort_criteria: impl IntoIterator + Clone, search_criteria: impl IntoIterator>, fetch_items: MacroOrMessageDataItemNames<'_>, uid: bool, ) -> Result>>, ClientError> { let mut fetch_items = match fetch_items { MacroOrMessageDataItemNames::Macro(m) => m.expand().into_static(), MacroOrMessageDataItemNames::MessageDataItemNames(items) => items, }; if uid && !fetch_items.contains(&MessageDataItemName::Uid) { fetch_items.push(MessageDataItemName::Uid); } let mut fetches = HashMap::new(); if self.state.ext_sort_supported() { let fetch_items = MacroOrMessageDataItemNames::MessageDataItemNames(fetch_items); let ids = self._sort(sort_criteria, search_criteria, uid).await?; let ids_chunks = ids.chunks(MAX_SEQUENCE_SIZE as usize); let ids_chunks_len = ids_chunks.len(); for (n, ids) in ids_chunks.enumerate() { debug!(?ids, "fetching sort envelopes {}/{ids_chunks_len}", n + 1); let ids = SequenceSet::try_from(ids.to_vec())?; let items = fetch_items.clone(); fetches.extend(self._fetch(ids, items, uid).await?); } let items = ids.into_iter().flat_map(|id| fetches.remove(&id)).collect(); Ok(items) } else { warn!("IMAP SORT extension not supported, using fallback"); let ids = self._search(search_criteria, uid).await?; let ids_chunks = ids.chunks(MAX_SEQUENCE_SIZE as usize); let ids_chunks_len = ids_chunks.len(); sort_criteria .clone() .into_iter() .filter_map(|criterion| match criterion.key { SortKey::Arrival => Some(MessageDataItemName::InternalDate), SortKey::Cc => Some(MessageDataItemName::Envelope), SortKey::Date => Some(MessageDataItemName::Envelope), SortKey::From => Some(MessageDataItemName::Envelope), SortKey::Size => Some(MessageDataItemName::Rfc822Size), SortKey::Subject => Some(MessageDataItemName::Envelope), SortKey::To => Some(MessageDataItemName::Envelope), SortKey::DisplayFrom => None, SortKey::DisplayTo => None, }) .for_each(|item| { if !fetch_items.contains(&item) { fetch_items.push(item) } }); for (n, ids) in ids_chunks.enumerate() { debug!(?ids, "fetching search envelopes {}/{ids_chunks_len}", n + 1); let ids = SequenceSet::try_from(ids.to_vec())?; let items = fetch_items.clone(); fetches.extend(self._fetch(ids, items.into(), uid).await?); } let mut fetches: Vec<_> = fetches.into_values().collect(); fetches.sort_by(|a, b| { for criterion in sort_criteria.clone().into_iter() { let mut cmp = cmp_fetch_items(&criterion.key, a, b); if criterion.reverse { cmp = cmp.reverse(); } if cmp.is_ne() { return cmp; } } cmp_fetch_items(&SortKey::Date, a, b) }); Ok(fetches) } } pub async fn sort_or_fallback( &mut self, sort_criteria: impl IntoIterator + Clone, search_criteria: impl IntoIterator>, fetch_items: MacroOrMessageDataItemNames<'_>, ) -> Result>>, ClientError> { self._sort_or_fallback(sort_criteria, search_criteria, fetch_items, false) .await } pub async fn uid_sort_or_fallback( &mut self, sort_criteria: impl IntoIterator + Clone, search_criteria: impl IntoIterator>, fetch_items: MacroOrMessageDataItemNames<'_>, ) -> Result>>, ClientError> { self._sort_or_fallback(sort_criteria, search_criteria, fetch_items, true) .await } pub async fn appenduid_or_fallback( &mut self, mailbox: impl TryInto, Error = ValidationError> + Clone, flags: impl IntoIterator>, message: impl AsRef<[u8]>, ) -> Result, ClientError> { if self.state.ext_uidplus_supported() { Ok(self .appenduid(mailbox, flags, message) .await? .map(|(uid, _)| uid)) } else { warn!("IMAP UIDPLUS extension not supported, using fallback"); // If the mailbox is currently selected, the normal new // message actions SHOULD occur. Specifically, the server // SHOULD 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. // // self.select(mailbox.clone()).await?; let seq = match self.append(mailbox, flags, message).await? { Some(seq) => seq, None => match self.post_append_noop().await? { Some(seq) => seq, None => self .post_append_check() .await? .ok_or(ClientError::ResolveTask(TaskError::MissingData( "APPENDUID: seq".into(), )))?, }, }; let uid = self .search(Vec1::from(SearchKey::SequenceSet(seq.try_into().unwrap()))) .await? .into_iter() .next(); Ok(uid) } } async fn _move_or_fallback( &mut self, sequence_set: SequenceSet, mailbox: impl TryInto, Error = ValidationError>, uid: bool, ) -> Result<(), ClientError> { if self.state.ext_move_supported() { self._move(sequence_set, mailbox, uid).await } else { warn!("IMAP MOVE extension not supported, using fallback"); self._copy(sequence_set.clone(), mailbox, uid).await?; self._silent_store(sequence_set, StoreType::Add, Some(Flag::Deleted), uid) .await?; self.expunge().await?; Ok(()) } } pub async fn move_or_fallback( &mut self, sequence_set: SequenceSet, mailbox: impl TryInto, Error = ValidationError>, ) -> Result<(), ClientError> { self._move_or_fallback(sequence_set, mailbox, false).await } pub async fn uid_move_or_fallback( &mut self, sequence_set: SequenceSet, mailbox: impl TryInto, Error = ValidationError>, ) -> Result<(), ClientError> { self._move_or_fallback(sequence_set, mailbox, true).await } pub fn enqueue_idle(&mut self) -> Tag<'static> { let tag = self.state.resolver.scheduler.tag_generator.generate(); self.state .resolver .scheduler .client_next .enqueue_command(Command { tag: tag.clone(), body: CommandBody::Idle, }); tag.into_static() } #[tracing::instrument(name = "idle", skip_all)] pub async fn idle(&mut self, tag: Tag<'static>) -> Result<(), stream::Error> { debug!("starting the main loop"); loop { let progress = self .stream .next(&mut self.state.resolver.scheduler.client_next); match timeout(self.state.idle_timeout, progress).await.ok() { None => { debug!("timed out, sending done command…"); self.state.resolver.scheduler.client_next.set_idle_done(); } Some(Err(err)) => { break Err(err); } Some(Ok(Event::IdleCommandSent { .. })) => { debug!("command sent"); } Some(Ok(Event::IdleAccepted { .. })) => { debug!("command accepted, entering idle mode"); } Some(Ok(Event::IdleRejected { status, .. })) => { warn!("command rejected, aborting: {status:?}"); break Ok(()); } Some(Ok(Event::IdleDoneSent { .. })) => { debug!("done command sent"); } Some(Ok(Event::DataReceived { data })) => { debug!("received data, sending done command…"); trace!("{data:#?}"); self.state.resolver.scheduler.client_next.set_idle_done(); } Some(Ok(Event::StatusReceived { status: Status::Tagged(Tagged { tag: ref got_tag, .. }), })) if *got_tag == tag => { debug!("received tagged response, exiting"); break Ok(()); } Some(event) => { debug!("received unknown event, ignoring: {event:?}"); } } } } #[tracing::instrument(name = "idle/done", skip_all)] pub async fn idle_done(&mut self, tag: Tag<'static>) -> Result<(), stream::Error> { self.state.resolver.scheduler.client_next.set_idle_done(); loop { let progress = self .stream .next(&mut self.state.resolver.scheduler.client_next) .await?; match progress { Event::IdleDoneSent { .. } => { debug!("done command sent"); } Event::StatusReceived { status: Status::Tagged(Tagged { tag: ref got_tag, .. }), } if *got_tag == tag => { debug!("received tagged response, exiting"); break Ok(()); } event => { debug!("received unknown event, ignoring: {event:?}"); } } } } } pub(crate) fn cmp_fetch_items( criterion: &SortKey, a: &Vec1, b: &Vec1, ) -> Ordering { use MessageDataItem::*; match &criterion { SortKey::Arrival => { let a = a.as_ref().iter().find_map(|a| { if let InternalDate(dt) = a { Some(dt.as_ref()) } else { None } }); let b = b.as_ref().iter().find_map(|b| { if let InternalDate(dt) = b { Some(dt.as_ref()) } else { None } }); a.cmp(&b) } SortKey::Date => { let a = a.as_ref().iter().find_map(|a| { if let Envelope(envelope) = a { envelope.date.0.as_ref().map(AsRef::as_ref) } else { None } }); let b = b.as_ref().iter().find_map(|b| { if let Envelope(envelope) = b { envelope.date.0.as_ref().map(AsRef::as_ref) } else { None } }); a.cmp(&b) } SortKey::Size => { let a = a.as_ref().iter().find_map(|a| { if let Rfc822Size(size) = a { Some(size) } else { None } }); let b = b.as_ref().iter().find_map(|b| { if let Rfc822Size(size) = b { Some(size) } else { None } }); a.cmp(&b) } SortKey::Subject => { let a = a.as_ref().iter().find_map(|a| { if let Envelope(envelope) = a { envelope.subject.0.as_ref().map(AsRef::as_ref) } else { None } }); let b = b.as_ref().iter().find_map(|b| { if let Envelope(envelope) = b { envelope.subject.0.as_ref().map(AsRef::as_ref) } else { None } }); a.cmp(&b) } // FIXME: Address missing Ord derive in imap-types SortKey::Cc | SortKey::From | SortKey::To | SortKey::DisplayFrom | SortKey::DisplayTo => { Ordering::Equal } } } pub(crate) fn to_static_literal( message: impl AsRef<[u8]>, ext_binary_supported: bool, ) -> Result, ValidationError> { let message = if ext_binary_supported { LiteralOrLiteral8::Literal8(Literal8 { data: message.as_ref().into(), mode: LiteralMode::Sync, }) } else { warn!("IMAP BINARY extension not supported, using fallback"); Literal::validate(message.as_ref())?; LiteralOrLiteral8::Literal(Literal::unvalidated(message.as_ref())) }; Ok(message.into_static()) } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/lib.rs000066400000000000000000000001271474047575500225540ustar00rootroot00000000000000pub mod client; pub mod stream; pub mod tasks; pub use imap_next::{self, imap_types}; imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/stream.rs000066400000000000000000000046321474047575500233060ustar00rootroot00000000000000use imap_next::{Interrupt, Io, State}; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tracing::trace; pub struct Stream { stream: S, buf: Vec, } impl Stream { pub fn new(stream: S) -> Self { Self { stream, buf: vec![0; 1024].into(), } } pub fn into_inner(self) -> S { self.stream } } impl Stream { pub async fn next(&mut self, mut state: F) -> Result> { let event = loop { // Progress the client/server let result = state.next(); // Return events immediately without doing IO let interrupt = match result { Err(interrupt) => interrupt, Ok(event) => break event, }; // Return errors immediately without doing IO let io = match interrupt { Interrupt::Io(io) => io, Interrupt::Error(err) => return Err(Error::State(err)), }; // Handle the output bytes from the client/server match io { Io::Output(ref bytes) => match self.stream.write(bytes).await? { 0 => return Err(Error::Closed), n => trace!("wrote {n}/{} bytes", bytes.len()), }, Io::NeedMoreInput => { trace!("more input needed"); } } match self.stream.read(&mut self.buf).await? { 0 => return Err(Error::Closed), n => { trace!("read {n}/{} bytes", self.buf.len()); state.enqueue_input(&self.buf[..n]); } } }; Ok(event) } } /// Error during reading into or writing from a stream. #[derive(Debug, Error)] pub enum Error { /// Operation failed because stream is closed. /// /// We detect this by checking if the read or written byte count is 0. Whether the stream is /// closed indefinitely or temporarily depends on the actual stream implementation. #[error("Stream was closed")] Closed, /// An I/O error occurred in the underlying stream. #[error(transparent)] Io(#[from] std::io::Error), /// An error occurred while progressing the state. #[error(transparent)] State(E), } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/000077500000000000000000000000001474047575500225655ustar00rootroot00000000000000imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/mod.rs000066400000000000000000000444301474047575500237170ustar00rootroot00000000000000pub mod resolver; pub mod tasks; use std::{ any::Any, collections::VecDeque, fmt::{Debug, Formatter}, marker::PhantomData, }; use imap_next::{ client::{Client as ClientNext, CommandHandle, Error, Event}, imap_types::{ auth::AuthenticateData, command::{Command, CommandBody}, core::{Tag, TagGenerator}, response::{ Bye, CommandContinuationRequest, Data, Greeting, Response, Status, StatusBody, Tagged, }, }, Interrupt, State, }; use thiserror::Error; use tracing::trace; /// Tells how a specific IMAP [`Command`] is processed. /// /// Most `process_` trait methods consume interesting responses (returning `None`), /// and move out uninteresting responses (returning `Some(...)`). /// /// If no active task is interested in a given response, we call this response "unsolicited". pub trait Task: Send + 'static { /// Output of the task. /// /// Returned in [`Self::process_tagged`]. type Output: Any + Send; /// Returns the [`CommandBody`] to issue for this task. /// /// Note: The [`Scheduler`] will tag the [`CommandBody`] creating a complete [`Command`]. fn command_body(&self) -> CommandBody<'static>; /// Process data response. fn process_data(&mut self, data: Data<'static>) -> Option> { // Default: Don't process server data Some(data) } /// Process untagged response. fn process_untagged( &mut self, status_body: StatusBody<'static>, ) -> Option> { // Default: Don't process untagged status Some(status_body) } /// Process command continuation request response. fn process_continuation_request( &mut self, continuation: CommandContinuationRequest<'static>, ) -> Option> { // Default: Don't process command continuation request response Some(continuation) } /// Process command continuation request response (during authenticate). fn process_continuation_request_authenticate( &mut self, continuation: CommandContinuationRequest<'static>, ) -> Result, CommandContinuationRequest<'static>> { // Default: Don't process command continuation request response (during authenticate) Err(continuation) } /// Process bye response. fn process_bye(&mut self, bye: Bye<'static>) -> Option> { // Default: Don't process bye Some(bye) } /// Process command completion result response. /// /// The [`Scheduler`] already chooses the corresponding response by tag. fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output; } /// Scheduler managing enqueued tasks and routing incoming responses to active tasks. pub struct Scheduler { pub client_next: ClientNext, waiting_tasks: TaskMap, active_tasks: TaskMap, pub tag_generator: TagGenerator, } impl Scheduler { /// Create a new scheduler. pub fn new(client_next: ClientNext) -> Self { Self { client_next, waiting_tasks: Default::default(), active_tasks: Default::default(), tag_generator: TagGenerator::new(), } } /// Enqueue a [`Task`]. pub fn enqueue_task(&mut self, task: T) -> TaskHandle where T: Task, { let tag = self.tag_generator.generate(); let command = { let body = task.command_body(); Command { tag: tag.clone(), body, } }; trace!(?command, "enqueue task"); let handle = self.client_next.enqueue_command(command); self.waiting_tasks.push_back(handle, tag, Box::new(task)); TaskHandle::new(handle) } pub fn enqueue_input(&mut self, bytes: &[u8]) { self.client_next.enqueue_input(bytes); } /// Progress the connection returning the next event. pub fn progress(&mut self) -> Result> { loop { let event = match self.client_next.next() { Ok(event) => event, Err(Interrupt::Io(io)) => return Err(Interrupt::Io(io)), Err(Interrupt::Error(err)) => { // HACK: skip bad fetches, improve me if let Error::MalformedMessage { discarded_bytes } = &err { let mut cmd = discarded_bytes.declassify().split(|c| c == &b' ').skip(2); if let Some(cmd) = cmd.next() { if cmd.eq_ignore_ascii_case(b"FETCH") { let fetch = String::from_utf8_lossy(discarded_bytes.declassify()); tracing::warn!(?fetch, "skipping invalid fetch"); continue; } } } return Err(Interrupt::Error(SchedulerError::Flow(err))); } }; match event { Event::GreetingReceived { greeting } => { return Ok(SchedulerEvent::GreetingReceived(greeting)); } Event::CommandSent { handle, .. } => { // This `unwrap` can't fail because `waiting_tasks` contains all unsent `Commands`. let (handle, tag, task) = self.waiting_tasks.remove_by_handle(handle).unwrap(); self.active_tasks.push_back(handle, tag, task); } Event::CommandRejected { handle, status, .. } => { let body = match status { Status::Tagged(Tagged { body, .. }) => body, _ => unreachable!(), }; // This `unwrap` can't fail because `active_tasks` contains all in-progress `Commands`. let (_, _, task) = self.active_tasks.remove_by_handle(handle).unwrap(); let output = Some(task.process_tagged(body)); return Ok(SchedulerEvent::TaskFinished(TaskToken { handle, output })); } Event::AuthenticateStarted { handle } => { let (handle, tag, task) = self.waiting_tasks.remove_by_handle(handle).unwrap(); self.active_tasks.push_back(handle, tag, task); } Event::AuthenticateContinuationRequestReceived { handle, continuation_request, } => { let task = self.active_tasks.get_task_by_handle_mut(handle).unwrap(); let continuation = task.process_continuation_request_authenticate(continuation_request); match continuation { Ok(data) => { self.client_next.set_authenticate_data(data).unwrap(); } Err(continuation) => { return Ok(SchedulerEvent::Unsolicited( Response::CommandContinuationRequest(continuation), )); } } } Event::AuthenticateStatusReceived { handle, status, .. } => { let (_, _, task) = self.active_tasks.remove_by_handle(handle).unwrap(); let body = match status { Status::Untagged(_) => unreachable!(), Status::Tagged(tagged) => tagged.body, Status::Bye(_) => unreachable!(), }; let output = Some(task.process_tagged(body)); return Ok(SchedulerEvent::TaskFinished(TaskToken { handle, output })); } Event::DataReceived { data } => { if let Some(data) = trickle_down(data, self.active_tasks.tasks_mut(), |task, data| { task.process_data(data) }) { return Ok(SchedulerEvent::Unsolicited(Response::Data(data))); } } Event::ContinuationRequestReceived { continuation_request, } => { if let Some(continuation) = trickle_down( continuation_request, self.active_tasks.tasks_mut(), |task, continuation_request| { task.process_continuation_request(continuation_request) }, ) { return Ok(SchedulerEvent::Unsolicited( Response::CommandContinuationRequest(continuation), )); } } Event::StatusReceived { status } => match status { Status::Untagged(body) => { if let Some(body) = trickle_down(body, self.active_tasks.tasks_mut(), |task, body| { task.process_untagged(body) }) { return Ok(SchedulerEvent::Unsolicited(Response::Status( Status::Untagged(body), ))); } } Status::Bye(bye) => { if let Some(bye) = trickle_down(bye, self.active_tasks.tasks_mut(), |task, bye| { task.process_bye(bye) }) { return Ok(SchedulerEvent::Unsolicited(Response::Status(Status::Bye( bye, )))); } } Status::Tagged(Tagged { tag, body }) => { let Some((handle, _, task)) = self.active_tasks.remove_by_tag(&tag) else { return Err(Interrupt::Error( SchedulerError::UnexpectedTaggedResponse(Tagged { tag, body }), )); }; let output = Some(task.process_tagged(body)); return Ok(SchedulerEvent::TaskFinished(TaskToken { handle, output })); } }, Event::IdleCommandSent { handle, .. } => { // This `unwrap` can't fail because `waiting_tasks` contains all unsent `Commands`. let (handle, tag, task) = self.waiting_tasks.remove_by_handle(handle).unwrap(); self.active_tasks.push_back(handle, tag, task); } Event::IdleAccepted { .. } => { println!("IDLE accepted!"); } Event::IdleRejected { handle, status, .. } => { let body = match status { Status::Tagged(Tagged { body, .. }) => body, _ => unreachable!(), }; // This `unwrap` can't fail because `active_tasks` contains all in-progress `Commands`. let (_, _, task) = self.active_tasks.remove_by_handle(handle).unwrap(); let output = Some(task.process_tagged(body)); return Ok(SchedulerEvent::TaskFinished(TaskToken { handle, output })); } Event::IdleDoneSent { .. } => { println!("IDLE done!"); } } } } } impl State for Scheduler { type Event = SchedulerEvent; type Error = SchedulerError; fn enqueue_input(&mut self, bytes: &[u8]) { self.enqueue_input(bytes); } fn next(&mut self) -> Result> { self.progress() } } #[derive(Default)] struct TaskMap { tasks: VecDeque<(CommandHandle, Tag<'static>, Box)>, } impl TaskMap { fn push_back(&mut self, handle: CommandHandle, tag: Tag<'static>, task: Box) { self.tasks.push_back((handle, tag, task)); } fn get_task_by_handle_mut(&mut self, handle: CommandHandle) -> Option<&mut Box> { self.tasks .iter_mut() .find_map(|(current_handle, _, task)| (handle == *current_handle).then_some(task)) } fn tasks_mut(&mut self) -> impl Iterator> { self.tasks.iter_mut().map(|(_, _, task)| task) } fn remove_by_handle( &mut self, handle: CommandHandle, ) -> Option<(CommandHandle, Tag<'static>, Box)> { let index = self .tasks .iter() .position(|(current_handle, _, _)| handle == *current_handle)?; self.tasks.remove(index) } fn remove_by_tag( &mut self, tag: &Tag, ) -> Option<(CommandHandle, Tag<'static>, Box)> { let index = self .tasks .iter() .position(|(_, current_tag, _)| tag == current_tag)?; self.tasks.remove(index) } } #[derive(Debug)] pub enum SchedulerEvent { GreetingReceived(Greeting<'static>), TaskFinished(TaskToken), Unsolicited(Response<'static>), } #[derive(Debug, Error)] pub enum SchedulerError { /// Flow error. #[error("flow error")] Flow(#[from] Error), /// Unexpected tag in command completion result. /// /// The scheduler received a tag that cannot be matched to an active command. /// This could be due to a severe implementation error in the scheduler, /// the server, or anything in-between, really. /// /// It's better to halt the execution to avoid damage. #[error("unexpected tag in command completion result")] UnexpectedTaggedResponse(Tagged<'static>), #[error("unexpected BYE response")] UnexpectedByeResponse(Bye<'static>), } #[derive(Eq)] pub struct TaskHandle { handle: CommandHandle, _t: PhantomData, } impl TaskHandle { fn new(handle: CommandHandle) -> Self { Self { handle, _t: Default::default(), } } /// Try resolving the task invalidating the token. /// /// The token is invalidated iff the return value is `Some`. pub fn resolve(&self, token: &mut TaskToken) -> Option { if token.handle != self.handle { return None; } let output = token.output.take()?; let output = output.downcast::().unwrap(); Some(*output) } } impl Debug for TaskHandle { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { f.debug_struct("TaskHandle") .field("handle", &self.handle) .finish() } } impl Clone for TaskHandle { fn clone(&self) -> Self { *self } } impl Copy for TaskHandle {} impl PartialEq for TaskHandle { fn eq(&self, other: &Self) -> bool { self.handle == other.handle } } #[derive(Debug)] pub struct TaskToken { handle: CommandHandle, output: Option>, } // ------------------------------------------------------------------------------------------------- /// Move `trickle` from consumer to consumer until the first consumer doesn't hand it back. /// /// If none of the consumers is interested in `trickle`, give it back. fn trickle_down(trickle: T, consumers: I, f: F) -> Option where I: Iterator, F: Fn(&mut I::Item, T) -> Option, { let mut trickle = Some(trickle); for mut consumer in consumers { if let Some(trickle_) = trickle { trickle = f(&mut consumer, trickle_); if trickle.is_none() { break; } } } trickle } // ------------------------------------------------------------------------------------------------- /// Helper trait that ... /// /// * doesn't have an associated type and uses [`Any`] in [`Self::process_tagged`] /// * is an object-safe "subset" of [`Task`] trait TaskAny: Send { fn process_data(&mut self, data: Data<'static>) -> Option>; fn process_untagged(&mut self, status_body: StatusBody<'static>) -> Option>; fn process_continuation_request( &mut self, continuation_request: CommandContinuationRequest<'static>, ) -> Option>; fn process_continuation_request_authenticate( &mut self, continuation_request: CommandContinuationRequest<'static>, ) -> Result, CommandContinuationRequest<'static>>; fn process_bye(&mut self, bye: Bye<'static>) -> Option>; fn process_tagged(self: Box, status_body: StatusBody<'static>) -> Box; } impl TaskAny for T where T: Task, { fn process_data(&mut self, data: Data<'static>) -> Option> { T::process_data(self, data) } fn process_untagged( &mut self, status_body: StatusBody<'static>, ) -> Option> { T::process_untagged(self, status_body) } fn process_continuation_request( &mut self, continuation_request: CommandContinuationRequest<'static>, ) -> Option> { T::process_continuation_request(self, continuation_request) } fn process_continuation_request_authenticate( &mut self, continuation_request: CommandContinuationRequest<'static>, ) -> Result, CommandContinuationRequest<'static>> { T::process_continuation_request_authenticate(self, continuation_request) } fn process_bye(&mut self, bye: Bye<'static>) -> Option> { T::process_bye(self, bye) } /// Returns [`Any`] instead of [`Task::Output`]. fn process_tagged(self: Box, status_body: StatusBody<'static>) -> Box { Box::new(T::process_tagged(*self, status_body)) } } #[cfg(test)] mod tests { use static_assertions::assert_impl_all; use super::Scheduler; assert_impl_all!(Scheduler: Send); } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/resolver.rs000066400000000000000000000050411474047575500247740ustar00rootroot00000000000000use imap_next::{ client::Client as ClientNext, imap_types::response::{Response, Status}, Interrupt, State, }; use tracing::{debug, warn}; use super::{Scheduler, SchedulerError, SchedulerEvent, Task, TaskHandle}; /// The resolver is a scheduler than manages one task at a time. pub struct Resolver { pub scheduler: Scheduler, } impl Resolver { /// Create a new resolver. pub fn new(client_next: ClientNext) -> Self { Self { scheduler: Scheduler::new(client_next), } } /// Enqueue a [`Task`] for immediate resolution. pub fn resolve(&mut self, task: T) -> ResolvingTask { let handle = self.scheduler.enqueue_task(task); ResolvingTask { resolver: self, handle, } } } impl State for Resolver { type Event = SchedulerEvent; type Error = SchedulerError; fn enqueue_input(&mut self, bytes: &[u8]) { self.scheduler.enqueue_input(bytes); } fn next(&mut self) -> Result> { self.scheduler.progress() } } pub struct ResolvingTask<'a, T: Task> { resolver: &'a mut Resolver, handle: TaskHandle, } impl State for ResolvingTask<'_, T> { type Event = T::Output; type Error = SchedulerError; fn enqueue_input(&mut self, bytes: &[u8]) { self.resolver.enqueue_input(bytes); } fn next(&mut self) -> Result> { loop { match self.resolver.next()? { SchedulerEvent::GreetingReceived(greeting) => { debug!("received greeting: {greeting:?}"); } SchedulerEvent::TaskFinished(mut token) => { if let Some(output) = self.handle.resolve(&mut token) { break Ok(output); } else { warn!(?token, "received unexpected task token") } } SchedulerEvent::Unsolicited(unsolicited) => { if let Response::Status(Status::Bye(bye)) = unsolicited { let err = SchedulerError::UnexpectedByeResponse(bye); break Err(Interrupt::Error(err)); } else { warn!(?unsolicited, "received unsolicited"); } } } } } } #[cfg(test)] mod tests { use static_assertions::assert_impl_all; use super::Resolver; assert_impl_all!(Resolver: Send); } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/000077500000000000000000000000001474047575500237125ustar00rootroot00000000000000imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/append.rs000066400000000000000000000114321474047575500255300ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, datetime::DateTime, extensions::binary::LiteralOrLiteral8, flag::Flag, mailbox::Mailbox, response::{Data, StatusBody, StatusKind}, }; use tracing::warn; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug)] pub struct AppendTask { mailbox: Mailbox<'static>, flags: Vec>, date: Option, message: LiteralOrLiteral8<'static>, output: Option, } impl AppendTask { pub fn new(mailbox: Mailbox<'static>, message: LiteralOrLiteral8<'static>) -> Self { Self { mailbox, flags: Default::default(), date: Default::default(), message, output: Default::default(), } } pub fn set_flags(&mut self, flags: Vec>) { self.flags = flags; } pub fn add_flag(&mut self, flag: Flag<'static>) { self.flags.push(flag); } pub fn with_flags(mut self, flags: Vec>) -> Self { self.set_flags(flags); self } pub fn with_flag(mut self, flag: Flag<'static>) -> Self { self.add_flag(flag); self } pub fn set_date(&mut self, date: DateTime) { self.date = Some(date); } pub fn with_date(mut self, date: DateTime) -> Self { self.set_date(date); self } } impl Task for AppendTask { type Output = Result, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Append { mailbox: self.mailbox.clone(), flags: self.flags.clone(), date: self.date.clone(), message: self.message.clone(), } } fn process_data(&mut self, data: Data<'static>) -> Option> { // In case the mailbox is already selected, we should receive // an `EXISTS` response. if let Data::Exists(seq) = data { if self.output.is_some() { warn!("received duplicate APPEND EXISTS data"); } self.output = Some(seq); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self.output), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } /// Special [`NoOpTask`](super::noop::NoOpTask) that captures `EXISTS` /// responses. /// /// This task should be used whenever [`AppendTask`] does not return /// the number of messages in the mailbox the appended message /// resides. #[derive(Clone, Debug, Default)] pub struct PostAppendNoOpTask { output: Option, } impl PostAppendNoOpTask { pub fn new() -> Self { Default::default() } } impl Task for PostAppendNoOpTask { type Output = Result, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Noop } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Exists(seq) = data { self.output = Some(seq); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self.output), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } /// Special [`CheckTask`](super::check::CheckTask) that captures /// `EXISTS` responses. /// /// This task should be used whenever [`AppendTask`] and /// [`PostAppendNoOpTask`] do not return the number of messages in the /// mailbox the appended message resides. #[derive(Clone, Debug, Default)] pub struct PostAppendCheckTask { output: Option, } impl PostAppendCheckTask { pub fn new() -> Self { Default::default() } } impl Task for PostAppendCheckTask { type Output = Result, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Check } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Exists(seq) = data { self.output = Some(seq); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self.output), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/appenduid.rs000066400000000000000000000043241474047575500262340ustar00rootroot00000000000000use std::num::NonZeroU32; use imap_next::imap_types::{ command::CommandBody, datetime::DateTime, extensions::binary::LiteralOrLiteral8, flag::Flag, mailbox::Mailbox, response::{Code, StatusBody, StatusKind}, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug)] pub struct AppendUidTask { mailbox: Mailbox<'static>, flags: Vec>, date: Option, message: LiteralOrLiteral8<'static>, } impl AppendUidTask { pub fn new(mailbox: Mailbox<'static>, message: LiteralOrLiteral8<'static>) -> Self { Self { mailbox, flags: Default::default(), date: Default::default(), message, } } pub fn set_flags(&mut self, flags: Vec>) { self.flags = flags; } pub fn add_flag(&mut self, flag: Flag<'static>) { self.flags.push(flag); } pub fn with_flags(mut self, flags: Vec>) -> Self { self.set_flags(flags); self } pub fn with_flag(mut self, flag: Flag<'static>) -> Self { self.add_flag(flag); self } pub fn set_date(&mut self, date: DateTime) { self.date = Some(date); } pub fn with_date(mut self, date: DateTime) -> Self { self.set_date(date); self } } impl Task for AppendUidTask { type Output = Result, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Append { mailbox: self.mailbox.clone(), flags: self.flags.clone(), date: self.date.clone(), message: self.message.clone(), } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => { if let Some(Code::AppendUid { uid, uid_validity }) = status_body.code { Ok(Some((uid, uid_validity))) } else { Ok(None) } } StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/authenticate.rs000066400000000000000000000104421474047575500267370ustar00rootroot00000000000000use std::borrow::Cow; use imap_next::imap_types::{ auth::{AuthMechanism, AuthenticateData}, command::CommandBody, core::Vec1, response::{Capability, Code, CommandContinuationRequest, Data, StatusBody, StatusKind}, secret::Secret, utils::escape_byte_string, }; use tracing::error; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug)] pub struct AuthenticateTask { /// Authentication mechanism. /// /// Note: Currently used for `AUTH=PLAIN`, `AUTH=OAUTHBEARER` and `AUTH=XOAUTH2`. /// Invariants need to be enforced through constructors. mechanism: AuthMechanism<'static>, /// Static authentication data. /// /// Note: Currently used for `AUTH=PLAIN`, `AUTH=OAUTHBEARER` and `AUTH=XOAUTH2`. line: Option>, /// Does the server support SASL's initial response? ir: bool, output: Option>>, } impl AuthenticateTask { pub fn plain(login: &str, passwd: &str, ir: bool) -> Self { let line = format!("\x00{login}\x00{passwd}"); Self { mechanism: AuthMechanism::Plain, line: Some(line.into_bytes()), ir, output: None, } } pub fn oauthbearer(user: &str, host: &str, port: u16, token: &str, ir: bool) -> Self { let line = format!("n,a={user},\x01host={host}\x01port={port}\x01auth=Bearer {token}\x01\x01"); Self { mechanism: AuthMechanism::OAuthBearer, line: Some(line.into_bytes()), ir, output: None, } } pub fn xoauth2(user: &str, token: &str, ir: bool) -> Self { let line = format!("user={user}\x01auth=Bearer {token}\x01\x01"); Self { mechanism: AuthMechanism::XOAuth2, line: Some(line.into_bytes()), ir, output: None, } } } impl Task for AuthenticateTask { type Output = Result>>, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Authenticate { mechanism: self.mechanism.clone(), initial_response: if self.ir { // TODO: command_body must only be called once... hm... Some(Secret::new(Cow::Owned(self.line.clone().unwrap()))) } else { None }, } } // Capabilities may (unfortunately) be found in a data response. // See https://github.com/modern-email/defects/issues/18 fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Capability(capabilities) = data { self.output = Some(capabilities); None } else { Some(data) } } fn process_continuation_request_authenticate( &mut self, continuation: CommandContinuationRequest<'static>, ) -> Result, CommandContinuationRequest<'static>> { let cancel = || match self.mechanism { AuthMechanism::XOAuth2 => { let err = match continuation { CommandContinuationRequest::Basic(data) => data.text().to_string(), CommandContinuationRequest::Base64(data) => escape_byte_string(data.as_ref()), }; error!("cannot authenticate using XOAUTH2 mechanism: {err}"); AuthenticateData::r#continue(vec![]) } _ => AuthenticateData::Cancel, }; if self.ir { return Ok(cancel()); } match self.line.take() { Some(data) => Ok(AuthenticateData::r#continue(data)), None => Ok(cancel()), } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok( // Capabilities may be found in the status body of tagged response. if let Some(Code::Capability(capabilities)) = status_body.code { Some(capabilities) } else { self.output }, ), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/capability.rs000066400000000000000000000041701474047575500264030ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, core::Vec1, response::{Capability, Code, Data, StatusBody, StatusKind}, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug, Default)] pub struct CapabilityTask { /// We use this as scratch space. output: Option>>, } impl CapabilityTask { pub fn new() -> Self { Default::default() } } impl Task for CapabilityTask { type Output = Result>, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Capability } // Capabilities may be found in a data response. fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Capability(capabilities) = data { self.output = Some(capabilities); None } else { Some(data) } } // Capabilities may (unfortunately) be found in a data response. // See https://github.com/modern-email/defects/issues/18 fn process_untagged( &mut self, status_body: StatusBody<'static>, ) -> Option> { if let Some(Code::Capability(capabilities)) = status_body.code { self.output = Some(capabilities); None } else { Some(status_body) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => match self.output { Some(capabilities) => Ok(capabilities), None => { // Capabilities may be found in the status body of tagged response. if let Some(Code::Capability(capabilities)) = status_body.code { Ok(capabilities) } else { Err(TaskError::MissingData("CAPABILITY".into())) } } }, StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/check.rs000066400000000000000000000014021474047575500253320ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, response::{StatusBody, StatusKind}, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug, Default)] pub struct CheckTask; impl CheckTask { pub fn new() -> Self { Default::default() } } impl Task for CheckTask { type Output = Result<(), TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Check } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(()), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/copy.rs000066400000000000000000000024111474047575500252300ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, mailbox::Mailbox, response::{StatusBody, StatusKind}, sequence::SequenceSet, }; use super::TaskError; use crate::tasks::Task; pub struct CopyTask { sequence_set: SequenceSet, mailbox: Mailbox<'static>, uid: bool, } impl CopyTask { pub fn new(sequence_set: SequenceSet, mailbox: Mailbox<'static>) -> Self { Self { sequence_set, mailbox, uid: true, } } pub fn set_uid(&mut self, uid: bool) { self.uid = uid; } pub fn with_uid(mut self, uid: bool) -> Self { self.set_uid(uid); self } } impl Task for CopyTask { type Output = Result<(), TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Copy { sequence_set: self.sequence_set.clone(), mailbox: self.mailbox.clone(), uid: self.uid, } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(()), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/create.rs000066400000000000000000000016041474047575500255240ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, mailbox::Mailbox, response::{StatusBody, StatusKind}, }; use crate::tasks::Task; use super::TaskError; #[derive(Clone, Debug)] pub struct CreateTask { mailbox: Mailbox<'static>, } impl CreateTask { pub fn new(mailbox: Mailbox<'static>) -> Self { Self { mailbox } } } impl Task for CreateTask { type Output = Result<(), TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Create { mailbox: self.mailbox.clone(), } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(()), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/delete.rs000066400000000000000000000016031474047575500255220ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, mailbox::Mailbox, response::{StatusBody, StatusKind}, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug)] pub struct DeleteTask { mailbox: Mailbox<'static>, } impl DeleteTask { pub fn new(mailbox: Mailbox<'static>) -> Self { Self { mailbox } } } impl Task for DeleteTask { type Output = Result<(), TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Delete { mailbox: self.mailbox.clone(), } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(()), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/enable.rs000066400000000000000000000027271474047575500255160ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, core::Vec1, extensions::enable::CapabilityEnable, response::{Data, StatusBody, StatusKind}, }; use crate::tasks::Task; use super::TaskError; #[derive(Clone, Debug)] pub struct EnableTask { requested_capabilities: Vec1>, enabled_capabilities: Option>>, } impl EnableTask { pub fn new(capabilities: Vec1>) -> Self { Self { requested_capabilities: capabilities, enabled_capabilities: None, } } } impl Task for EnableTask { type Output = Result>>, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Enable { capabilities: self.requested_capabilities.clone(), } } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Enabled { capabilities } = data { self.enabled_capabilities = Some(capabilities); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self.enabled_capabilities), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/expunge.rs000066400000000000000000000031711474047575500257350ustar00rootroot00000000000000use std::num::NonZeroU32; use imap_next::imap_types::{ command::CommandBody, response::{Data, StatusBody, StatusKind}, }; use super::TaskError; use crate::tasks::Task; /// Permanently removes messages containing the Deleted flag in their /// envelope. /// /// Be aware that the returned vector can contain multiple time the /// same sequence number, depending on the server implementation /// style: /// /// > 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 #[derive(Clone, Debug, Default)] pub struct ExpungeTask { output: Vec, } impl ExpungeTask { pub fn new() -> Self { Default::default() } } impl Task for ExpungeTask { type Output = Result, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Expunge } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Expunge(seq) = data { self.output.push(seq); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self.output), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/fetch.rs000066400000000000000000000104071474047575500253530ustar00rootroot00000000000000use std::{ collections::{HashMap, HashSet}, num::NonZeroU32, }; use imap_next::imap_types::{ command::CommandBody, core::Vec1, fetch::{MacroOrMessageDataItemNames, MessageDataItem}, response::{Data, StatusBody, StatusKind}, sequence::{SeqOrUid, SequenceSet}, }; use tracing::warn; use super::TaskError; use crate::tasks::Task; /// Fetch message data items matching the given sequence set and the /// given item names (or macro). #[derive(Clone, Debug)] pub struct FetchTask { sequence_set: SequenceSet, macro_or_item_names: MacroOrMessageDataItemNames<'static>, uid: bool, output: HashMap>>, } impl FetchTask { pub fn new(sequence_set: SequenceSet, items: MacroOrMessageDataItemNames<'static>) -> Self { Self { sequence_set, macro_or_item_names: items, uid: true, output: Default::default(), } } pub fn set_uid(&mut self, uid: bool) { self.uid = uid; } pub fn with_uid(mut self, uid: bool) -> Self { self.set_uid(uid); self } } impl Task for FetchTask { type Output = Result>>, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Fetch { sequence_set: self.sequence_set.clone(), macro_or_item_names: self.macro_or_item_names.clone(), uid: self.uid, } } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Fetch { items, seq } = data { if let Some(prev_items) = self.output.get_mut(&seq) { warn!(?prev_items, next_items = ?items, "received additional items for {seq}"); prev_items.extend(items.into_iter()); } else { self.output.insert(seq, items.into_iter().collect()); } None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self .output .into_iter() .map(|(key, val)| (key, Vec1::unvalidated(val.into_iter().collect()))) .collect()), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } /// Same as [`FetchTask`], except that it only collects message data /// items for the first message matching the given id. #[derive(Clone, Debug)] pub struct FetchFirstTask { id: NonZeroU32, macro_or_item_names: MacroOrMessageDataItemNames<'static>, uid: bool, output: Option>>, } impl FetchFirstTask { pub fn new(id: NonZeroU32, items: MacroOrMessageDataItemNames<'static>) -> Self { Self { id, macro_or_item_names: items, uid: true, output: None, } } pub fn set_uid(&mut self, uid: bool) { self.uid = uid; } pub fn with_uid(mut self, uid: bool) -> Self { self.set_uid(uid); self } } impl Task for FetchFirstTask { type Output = Result>, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Fetch { sequence_set: SequenceSet::from(SeqOrUid::from(self.id)), macro_or_item_names: self.macro_or_item_names.clone(), uid: self.uid, } } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Fetch { items, .. } = data { self.output = Some(items); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => match self.output { Some(items) => Ok(items), None => Err(TaskError::MissingData("FETCH: items".into())), }, StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/id.rs000066400000000000000000000025601474047575500246570ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, core::{IString, NString}, response::{Data, StatusBody, StatusKind}, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug, Default)] pub struct IdTask { client: Option, NString<'static>)>>, server: Option, NString<'static>)>>, } impl IdTask { pub fn new(parameters: Option, NString<'static>)>>) -> Self { Self { client: parameters, server: None, } } } impl Task for IdTask { type Output = Result, NString<'static>)>>, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Id { parameters: self.client.clone(), } } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Id { parameters } = data { self.server = parameters; None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self.server), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/list.rs000066400000000000000000000034051474047575500252350ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, core::QuotedChar, flag::FlagNameAttribute, mailbox::{ListMailbox, Mailbox}, response::{Data, StatusBody, StatusKind}, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug)] pub struct ListTask { mailbox: Mailbox<'static>, mailbox_wildcard: ListMailbox<'static>, output: Vec<( Mailbox<'static>, Option, Vec>, )>, } impl ListTask { pub fn new(mailbox: Mailbox<'static>, mailbox_wildcard: ListMailbox<'static>) -> Self { Self { mailbox, mailbox_wildcard, output: Vec::new(), } } } impl Task for ListTask { type Output = Result< Vec<( Mailbox<'static>, Option, Vec>, )>, TaskError, >; fn command_body(&self) -> CommandBody<'static> { CommandBody::List { reference: self.mailbox.clone(), mailbox_wildcard: self.mailbox_wildcard.clone(), } } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::List { items, delimiter, mailbox, } = data { self.output.push((mailbox, delimiter, items)); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self.output), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/login.rs000066400000000000000000000034201474047575500253670ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, core::{AString, Vec1}, response::{Capability, Code, Data, StatusBody, StatusKind}, secret::Secret, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug)] pub struct LoginTask { username: AString<'static>, password: Secret>, output: Option>>, } impl LoginTask { pub fn new(username: AString<'static>, password: Secret>) -> Self { Self { username, password, output: None, } } } impl Task for LoginTask { type Output = Result>>, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Login { username: self.username.clone(), password: self.password.clone(), } } // Capabilities may (unfortunately) be found in a data response. // See https://github.com/modern-email/defects/issues/18 fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Capability(capabilities) = data { self.output = Some(capabilities); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok( if let Some(Code::Capability(capabilities)) = status_body.code { Some(capabilities) } else { self.output }, ), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/logout.rs000066400000000000000000000021211474047575500255650ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, response::{Bye, StatusBody, StatusKind}, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug, Default)] pub struct LogoutTask { got_bye: bool, } impl LogoutTask { pub fn new() -> Self { Default::default() } } impl Task for LogoutTask { type Output = Result<(), TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Logout } fn process_bye(&mut self, _: Bye<'static>) -> Option> { self.got_bye = true; None } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => { if self.got_bye { Ok(()) } else { Err(TaskError::MissingData("LOGOUT: BYE".into())) } } StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/mod.rs000066400000000000000000000013571474047575500250450ustar00rootroot00000000000000use imap_next::imap_types::response::StatusBody; use thiserror::Error; pub mod append; pub mod appenduid; pub mod authenticate; pub mod capability; pub mod check; pub mod copy; pub mod create; pub mod delete; pub mod enable; pub mod expunge; pub mod fetch; pub mod id; pub mod list; pub mod login; pub mod logout; pub mod r#move; pub mod noop; pub mod search; pub mod select; pub mod sort; pub mod store; pub mod thread; #[derive(Debug, Error)] pub enum TaskError { #[error("unexpected BAD response: {}", .0.text)] UnexpectedBadResponse(StatusBody<'static>), #[error("unexpected NO response: {}", .0.text)] UnexpectedNoResponse(StatusBody<'static>), #[error("missing required data for command {0}")] MissingData(String), } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/move.rs000066400000000000000000000024111474047575500252240ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, mailbox::Mailbox, response::{StatusBody, StatusKind}, sequence::SequenceSet, }; use super::TaskError; use crate::tasks::Task; pub struct MoveTask { sequence_set: SequenceSet, mailbox: Mailbox<'static>, uid: bool, } impl MoveTask { pub fn new(sequence_set: SequenceSet, mailbox: Mailbox<'static>) -> Self { Self { sequence_set, mailbox, uid: true, } } pub fn set_uid(&mut self, uid: bool) { self.uid = uid; } pub fn with_uid(mut self, uid: bool) -> Self { self.set_uid(uid); self } } impl Task for MoveTask { type Output = Result<(), TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Move { sequence_set: self.sequence_set.clone(), mailbox: self.mailbox.clone(), uid: self.uid, } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(()), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/noop.rs000066400000000000000000000013761474047575500252420ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, response::{StatusBody, StatusKind}, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug, Default)] pub struct NoOpTask; impl NoOpTask { pub fn new() -> Self { Default::default() } } impl Task for NoOpTask { type Output = Result<(), TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Noop } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(()), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/search.rs000066400000000000000000000033221474047575500255250ustar00rootroot00000000000000use std::num::NonZeroU32; use imap_next::imap_types::{ command::CommandBody, core::Vec1, response::{Data, StatusBody, StatusKind}, search::SearchKey, }; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug)] pub struct SearchTask { criteria: Vec1>, uid: bool, output: Vec, } impl SearchTask { pub fn new(criteria: Vec1>) -> Self { Self { criteria, ..Default::default() } } pub fn set_uid(&mut self, uid: bool) { self.uid = uid; } pub fn with_uid(mut self, uid: bool) -> Self { self.set_uid(uid); self } } impl Default for SearchTask { fn default() -> Self { Self { criteria: Vec1::from(SearchKey::All), uid: true, output: Default::default(), } } } impl Task for SearchTask { type Output = Result, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Search { charset: None, criteria: self.criteria.clone(), uid: self.uid, } } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Search(ids) = data { self.output = ids; None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self.output), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/select.rs000066400000000000000000000102101474047575500255310ustar00rootroot00000000000000use std::num::NonZeroU32; use imap_next::imap_types::{ command::CommandBody, flag::{Flag, FlagPerm}, mailbox::Mailbox, response::{Code, Data, StatusBody, StatusKind}, }; use tracing::warn; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug, Default)] pub struct SelectDataUnvalidated { // required untagged responses pub flags: Option>>, pub exists: Option, pub recent: Option, // required OK untagged responses pub unseen: Option, pub permanent_flags: Option>>, pub uid_next: Option, pub uid_validity: Option, } impl SelectDataUnvalidated { pub fn validate(self) -> Result { if self.flags.is_none() { warn!("missing required FLAGS untagged response"); } if self.exists.is_none() { warn!("missing required EXISTS untagged response"); } if self.recent.is_none() { warn!("missing required RECENT untagged response"); } if self.unseen.is_none() { warn!("missing required UNSEEN OK untagged response"); } if self.permanent_flags.is_none() { warn!("missing required PERMANENTFLAGS OK untagged response"); } if self.uid_next.is_none() { warn!("missing required UIDNEXT OK untagged response"); } if self.uid_validity.is_none() { warn!("missing required UIDVALIDITY OK untagged response"); } Ok(self) } } #[derive(Clone, Debug)] pub struct SelectTask { mailbox: Mailbox<'static>, read_only: bool, output: SelectDataUnvalidated, } impl SelectTask { pub fn new(mailbox: Mailbox<'static>) -> Self { Self { mailbox, read_only: false, output: Default::default(), } } pub fn read_only(mailbox: Mailbox<'static>) -> Self { Self { mailbox, read_only: true, output: Default::default(), } } } impl Task for SelectTask { type Output = Result; fn command_body(&self) -> CommandBody<'static> { let mailbox = self.mailbox.clone(); if self.read_only { CommandBody::Examine { mailbox } } else { CommandBody::Select { mailbox } } } fn process_data(&mut self, data: Data<'static>) -> Option> { match data { Data::Flags(flags) => { self.output.flags = Some(flags); None } Data::Exists(count) => { self.output.exists = Some(count); None } Data::Recent(count) => { self.output.recent = Some(count); None } data => Some(data), } } fn process_untagged( &mut self, status_body: StatusBody<'static>, ) -> Option> { if let StatusKind::Ok = status_body.kind { match status_body.code { Some(Code::Unseen(seq)) => { self.output.unseen = Some(seq); None } Some(Code::PermanentFlags(flags)) => { self.output.permanent_flags = Some(flags); None } Some(Code::UidNext(uid)) => { self.output.uid_next = Some(uid); None } Some(Code::UidValidity(uid)) => { self.output.uid_validity = Some(uid); None } _ => Some(status_body), } } else { Some(status_body) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => self.output.validate(), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/sort.rs000066400000000000000000000045761474047575500252630ustar00rootroot00000000000000use std::num::NonZeroU32; use imap_next::imap_types::{ command::CommandBody, core::{Charset, Vec1}, extensions::sort::SortCriterion, response::{Data, StatusBody, StatusKind}, search::SearchKey, }; use tracing::warn; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug)] pub struct SortTask { sort_criteria: Vec1, charset: Charset<'static>, search_criteria: Vec1>, uid: bool, output: Option>, } impl SortTask { pub fn new( sort_criteria: Vec1, search_criteria: Vec1>, ) -> Self { Self { sort_criteria, charset: Charset::try_from("UTF-8").unwrap(), search_criteria, uid: true, output: Default::default(), } } pub fn set_charset(&mut self, charset: Charset<'static>) { self.charset = charset; } pub fn with_charset(mut self, charset: Charset<'static>) -> Self { self.set_charset(charset); self } pub fn set_uid(&mut self, uid: bool) { self.uid = uid; } pub fn with_uid(mut self, uid: bool) -> Self { self.set_uid(uid); self } } impl Task for SortTask { type Output = Result, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Sort { sort_criteria: self.sort_criteria.clone(), charset: self.charset.clone(), search_criteria: self.search_criteria.clone(), uid: self.uid, } } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Sort(ids) = data { if self.output.is_some() { warn!("received duplicate sort data"); } self.output = Some(ids); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => match self.output { Some(output) => Ok(output), None => Err(TaskError::MissingData("SORT".into())), }, StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/store.rs000066400000000000000000000062371474047575500254240ustar00rootroot00000000000000use std::{collections::HashMap, num::NonZeroU32}; use imap_next::imap_types::{ command::CommandBody, core::Vec1, fetch::MessageDataItem, flag::{Flag, StoreResponse, StoreType}, response::{Data, StatusBody, StatusKind}, sequence::SequenceSet, }; use tracing::warn; use super::TaskError; use crate::tasks::Task; /// Alter message data. #[derive(Clone, Debug)] pub struct StoreTask { sequence_set: SequenceSet, kind: StoreType, flags: Vec>, uid: bool, output: HashMap>>, } impl StoreTask { pub fn new(sequence_set: SequenceSet, kind: StoreType, flags: Vec>) -> Self { Self { sequence_set, kind, flags, uid: true, output: Default::default(), } } pub fn set_uid(&mut self, uid: bool) { self.uid = uid; } pub fn with_uid(mut self, uid: bool) -> Self { self.set_uid(uid); self } pub fn silent(self) -> SilentStoreTask { SilentStoreTask::new(self) } } impl Task for StoreTask { type Output = Result>>, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Store { sequence_set: self.sequence_set.clone(), kind: self.kind, response: StoreResponse::Answer, flags: self.flags.clone(), uid: self.uid, } } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Fetch { items, seq } = data { if let Some(items) = self.output.insert(seq, items) { warn!(seq, ?items, "received duplicate items"); } None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(self.output), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } /// Alter message data instructing the server to not send the updated values. /// /// Note: Same as [`StoreTask`], except that it does not return any output. #[derive(Clone, Debug)] pub struct SilentStoreTask(StoreTask); impl SilentStoreTask { pub fn new(store: StoreTask) -> Self { Self(store) } } impl Task for SilentStoreTask { type Output = Result<(), TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Store { sequence_set: self.0.sequence_set.clone(), kind: self.0.kind, response: StoreResponse::Silent, flags: self.0.flags.clone(), uid: self.0.uid, } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => Ok(()), StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } } imap-client-5e26ef596abd232f4f42be1c205531c0af138848/src/tasks/tasks/thread.rs000066400000000000000000000045741474047575500255410ustar00rootroot00000000000000use imap_next::imap_types::{ command::CommandBody, core::{Charset, Vec1}, extensions::thread::{Thread, ThreadingAlgorithm}, response::{Data, StatusBody, StatusKind}, search::SearchKey, }; use tracing::warn; use super::TaskError; use crate::tasks::Task; #[derive(Clone, Debug)] pub struct ThreadTask { algorithm: ThreadingAlgorithm<'static>, charset: Charset<'static>, search_criteria: Vec1>, uid: bool, output: Option>, } impl ThreadTask { pub fn new( algorithm: ThreadingAlgorithm<'static>, search_criteria: Vec1>, ) -> Self { Self { algorithm, charset: Charset::try_from("UTF-8").unwrap(), search_criteria, uid: true, output: Default::default(), } } pub fn set_charset(&mut self, charset: Charset<'static>) { self.charset = charset; } pub fn with_charset(mut self, charset: Charset<'static>) -> Self { self.set_charset(charset); self } pub fn set_uid(&mut self, uid: bool) { self.uid = uid; } pub fn with_uid(mut self, uid: bool) -> Self { self.set_uid(uid); self } } impl Task for ThreadTask { type Output = Result, TaskError>; fn command_body(&self) -> CommandBody<'static> { CommandBody::Thread { algorithm: self.algorithm.clone(), charset: self.charset.clone(), search_criteria: self.search_criteria.clone(), uid: self.uid, } } fn process_data(&mut self, data: Data<'static>) -> Option> { if let Data::Thread(threads) = data { if self.output.is_some() { warn!("received duplicate thread data"); } self.output = Some(threads); None } else { Some(data) } } fn process_tagged(self, status_body: StatusBody<'static>) -> Self::Output { match status_body.kind { StatusKind::Ok => match self.output { Some(output) => Ok(output), None => Err(TaskError::MissingData("SORT".into())), }, StatusKind::No => Err(TaskError::UnexpectedNoResponse(status_body)), StatusKind::Bad => Err(TaskError::UnexpectedBadResponse(status_body)), } } }