deltachat/imap/
idle.rs

1use std::time::Duration;
2
3use anyhow::{Context as _, Result};
4use async_channel::Receiver;
5use async_imap::extensions::idle::IdleResponse;
6use tokio::time::timeout;
7
8use super::session::Session;
9use super::Imap;
10use crate::context::Context;
11use crate::net::TIMEOUT;
12use crate::tools::{self, time_elapsed};
13
14/// Timeout after which IDLE is finished
15/// if there are no responses from the server.
16///
17/// If `* OK Still here` keepalives are sent more frequently
18/// than this duration, timeout should never be triggered.
19/// For example, Dovecot sends keepalives every 2 minutes by default.
20const IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
21
22impl Session {
23    pub async fn idle(
24        mut self,
25        context: &Context,
26        idle_interrupt_receiver: Receiver<()>,
27        folder: &str,
28    ) -> Result<Self> {
29        let create = true;
30        self.select_with_uidvalidity(context, folder, create)
31            .await?;
32
33        if self.drain_unsolicited_responses(context)? {
34            self.new_mail = true;
35        }
36
37        if self.new_mail {
38            info!(
39                context,
40                "Skipping IDLE in {folder:?} because there may be new mail."
41            );
42            return Ok(self);
43        }
44
45        if let Ok(()) = idle_interrupt_receiver.try_recv() {
46            info!(context, "Skip IDLE in {folder:?} because we got interrupt.");
47            return Ok(self);
48        }
49
50        let mut handle = self.inner.idle();
51        handle
52            .init()
53            .await
54            .with_context(|| format!("IMAP IDLE protocol failed to init in folder {folder:?}"))?;
55
56        // At this point IDLE command was sent and we received a "+ idling" response. We will now
57        // read from the stream without getting any data for up to `IDLE_TIMEOUT`. If we don't
58        // disable read timeout, we would get a timeout after `crate::net::TIMEOUT`, which is a lot
59        // shorter than `IDLE_TIMEOUT`.
60        handle.as_mut().set_read_timeout(None);
61        let (idle_wait, interrupt) = handle.wait_with_timeout(IDLE_TIMEOUT);
62
63        info!(
64            context,
65            "IDLE entering wait-on-remote state in folder {folder:?}."
66        );
67
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();
76
77                info!(context, "{folder:?}: Received interrupt, stopping IDLE.");
78
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)) => {
86                info!(context, "{folder:?}: Idle has NewData {x:?}");
87            }
88            Ok(IdleResponse::Timeout) => {
89                info!(context, "{folder:?}: Idle-wait timeout or interruption.");
90            }
91            Ok(IdleResponse::ManualInterrupt) => {
92                info!(context, "{folder:?}: Idle wait was interrupted manually.");
93            }
94            Err(err) => {
95                warn!(context, "{folder:?}: Idle wait errored: {err:?}.");
96            }
97        }
98
99        // Abort the task, then await to ensure the future is dropped.
100        interrupt_relay.abort();
101        interrupt_relay.await.ok();
102
103        let mut session = tokio::time::timeout(Duration::from_secs(15), handle.done())
104            .await
105            .with_context(|| format!("{folder}: IMAP IDLE protocol timed out"))?
106            .with_context(|| format!("{folder}: IMAP IDLE failed"))?;
107        session.as_mut().set_read_timeout(Some(TIMEOUT));
108        self.inner = session;
109
110        // Fetch mail once we exit IDLE.
111        self.new_mail = true;
112
113        Ok(self)
114    }
115}
116
117impl Imap {
118    /// Idle using polling.
119    pub(crate) async fn fake_idle(
120        &mut self,
121        context: &Context,
122        watch_folder: String,
123    ) -> Result<()> {
124        let fake_idle_start_time = tools::Time::now();
125
126        info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
127
128        // Wait for 60 seconds or until we are interrupted.
129        match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
130            Err(_) => info!(context, "Fake IDLE finished."),
131            Ok(_) => info!(context, "Fake IDLE interrupted."),
132        }
133
134        info!(
135            context,
136            "IMAP-fake-IDLE done after {:.4}s",
137            time_elapsed(&fake_idle_start_time).as_millis() as f64 / 1000.,
138        );
139        Ok(())
140    }
141}