refactor(channel): replace hand-rolled IMAP with async-imap IDLE

Replace the blocking, poll-based IMAP client with async-imap and
IMAP IDLE (RFC 2177) for instant push delivery. Key changes:

- Add async-imap dependency with tokio runtime feature
- Rewrite connect/fetch/listen paths to fully async using tokio TLS
- Implement IDLE loop with exponential backoff reconnection (1s–60s cap)
- Add idle_timeout_secs config field (default 1740s per RFC 2177)
- Convert health_check to async connect-and-logout with 10s timeout
- Update affected tests from sync to #[tokio::test]

SMTP send path, allowlist enforcement, and Channel trait contract
are unchanged.
This commit is contained in:
Kieran 2026-02-18 10:44:18 +00:00 committed by Chummy
parent 08ea559c21
commit 5d9e8705ac
3 changed files with 474 additions and 236 deletions

151
Cargo.lock generated
View file

@ -145,6 +145,64 @@ dependencies = [
"object 0.37.3",
]
[[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.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-compression"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f"
dependencies = [
"compression-codecs",
"compression-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-imap"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78dceaba06f029d8f4d7df20addd4b7370a30206e3926267ecda2915b0f3f66"
dependencies = [
"async-channel 2.5.0",
"async-compression",
"base64",
"bytes",
"chrono",
"futures",
"imap-proto",
"log",
"nom 7.1.3",
"pin-project",
"pin-utils",
"self_cell",
"stop-token",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "async-io"
version = "2.6.0"
@ -602,6 +660,22 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "compression-codecs"
version = "0.4.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a"
dependencies = [
"compression-core",
"flate2",
]
[[package]]
name = "compression-core"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@ -1130,6 +1204,33 @@ dependencies = [
"num-traits",
]
[[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.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener 5.4.1",
"pin-project-lite",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
@ -1875,6 +1976,15 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "365a784774bb381e8c19edb91190a90d7f2625e057b55de2bc0f6b57bc779ff2"
[[package]]
name = "imap-proto"
version = "0.16.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba1f9b30846c3d04371159ef3a0413ce7c1ae0a8c619cd255c60b3d902553f22"
dependencies = [
"nom 7.1.3",
]
[[package]]
name = "indexmap"
version = "2.13.0"
@ -2041,7 +2151,7 @@ dependencies = [
"httpdate",
"idna",
"mime",
"nom",
"nom 8.0.0",
"percent-encoding",
"quoted_printable",
"rustls",
@ -2156,7 +2266,7 @@ dependencies = [
"itoa",
"log",
"md-5",
"nom",
"nom 8.0.0",
"nom_locate",
"rand 0.9.2",
"rangemap",
@ -2260,6 +2370,12 @@ dependencies = [
"unicase",
]
[[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.9"
@ -2350,6 +2466,16 @@ dependencies = [
"memchr",
]
[[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 = "nom"
version = "8.0.0"
@ -2367,7 +2493,7 @@ checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
dependencies = [
"bytecount",
"memchr",
"nom",
"nom 8.0.0",
]
[[package]]
@ -3390,6 +3516,12 @@ dependencies = [
"libc",
]
[[package]]
name = "self_cell"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
[[package]]
name = "semver"
version = "1.0.27"
@ -3662,6 +3794,18 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "stop-token"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b"
dependencies = [
"async-channel 1.9.0",
"cfg-if",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "stringprep"
version = "0.1.5"
@ -5176,6 +5320,7 @@ name = "zeroclaw"
version = "0.1.0"
dependencies = [
"anyhow",
"async-imap",
"async-trait",
"axum",
"base64",