Skip to content

Commit 81e9628

Browse files
committed
refactor: do not cancel the task returned from async_imap Handle.wait_with_timeout
This task is not guaranteed to be cancellation-safe and provides a stop token for safe cancellation instead. We should always cancel the task properly and not by racing against another future. Otherwise following call to Handle.done() may work on IMAP session that is in the middle of response, for example.
1 parent aaa0296 commit 81e9628

File tree

1 file changed

+22
-21
lines changed

1 file changed

+22
-21
lines changed

src/imap/idle.rs

+22-21
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::time::Duration;
33
use anyhow::{Context as _, Result};
44
use async_channel::Receiver;
55
use async_imap::extensions::idle::IdleResponse;
6-
use futures_lite::FutureExt;
76
use tokio::time::timeout;
87

98
use super::session::Session;
@@ -27,8 +26,6 @@ impl Session {
2726
idle_interrupt_receiver: Receiver<()>,
2827
folder: &str,
2928
) -> Result<Self> {
30-
use futures::future::FutureExt;
31-
3229
let create = true;
3330
self.select_with_uidvalidity(context, folder, create)
3431
.await?;
@@ -63,42 +60,46 @@ impl Session {
6360
handle.as_mut().set_read_timeout(None);
6461
let (idle_wait, interrupt) = handle.wait_with_timeout(IDLE_TIMEOUT);
6562

66-
enum Event {
67-
IdleResponse(IdleResponse),
68-
Interrupt,
69-
}
70-
7163
info!(
7264
context,
7365
"IDLE entering wait-on-remote state in folder {folder:?}."
7466
);
75-
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
76-
idle_interrupt_receiver.recv().await.ok();
7767

78-
// cancel imap idle connection properly
79-
drop(interrupt);
68+
// Spawn a task to relay interrupts from `idle_interrupt_receiver`
69+
// into interruptions of IDLE.
70+
let interrupt_relay = {
71+
let context = context.clone();
72+
let folder = folder.to_string();
73+
74+
tokio::spawn(async move {
75+
idle_interrupt_receiver.recv().await.ok();
8076

81-
Ok(Event::Interrupt)
82-
});
77+
info!(context, "{folder:?}: Received interrupt, stopping IDLE.");
8378

84-
match fut.await {
85-
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
79+
// Drop `interrupt` in order to stop the IMAP IDLE.
80+
drop(interrupt);
81+
})
82+
};
83+
84+
match idle_wait.await {
85+
Ok(IdleResponse::NewData(x)) => {
8686
info!(context, "{folder:?}: Idle has NewData {x:?}");
8787
}
88-
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
88+
Ok(IdleResponse::Timeout) => {
8989
info!(context, "{folder:?}: Idle-wait timeout or interruption.");
9090
}
91-
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
91+
Ok(IdleResponse::ManualInterrupt) => {
9292
info!(context, "{folder:?}: Idle wait was interrupted manually.");
9393
}
94-
Ok(Event::Interrupt) => {
95-
info!(context, "{folder:?}: Idle wait was interrupted.");
96-
}
9794
Err(err) => {
9895
warn!(context, "{folder:?}: Idle wait errored: {err:?}.");
9996
}
10097
}
10198

99+
// Abort the task, then await to ensure the future is dropped.
100+
interrupt_relay.abort();
101+
interrupt_relay.await.ok();
102+
102103
let mut session = tokio::time::timeout(Duration::from_secs(15), handle.done())
103104
.await
104105
.with_context(|| format!("{folder}: IMAP IDLE protocol timed out"))?

0 commit comments

Comments
 (0)