refactor(imap): remove Session from Imap structure

Connection establishment now happens only in one place in each IMAP loop.
Now all connection establishment happens in one place
and is limited by the ratelimit.

Backoff was removed from fake_idle
as it does not establish connections anymore.
If connection fails, fake_idle will return an error.
We then drop the connection and get back to the beginning of IMAP
loop.

Backoff may be still nice to have to delay retries
in case of constant connection failures
so we don't immediately hit ratelimit if the network is unusable
and returns immediate error on each connection attempt
(e.g. ICMP network unreachable error),
but adding backoff for connection failures is out of scope for this change.
This commit is contained in:
link2xt
2024-02-29 02:43:48 +00:00
parent b08a4d6fcf
commit 07870a6d69
7 changed files with 254 additions and 389 deletions

View File

@@ -25,7 +25,7 @@ use tokio::task;
use crate::config::{self, Config}; use crate::config::{self, Config};
use crate::contact::addr_cmp; use crate::contact::addr_cmp;
use crate::context::Context; use crate::context::Context;
use crate::imap::Imap; use crate::imap::{session::Session as ImapSession, Imap};
use crate::log::LogExt; use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam}; use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype}; use crate::message::{Message, Viewtype};
@@ -395,7 +395,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// Configure IMAP // Configure IMAP
let mut imap: Option<Imap> = None; let mut imap: Option<(Imap, ImapSession)> = None;
let imap_servers: Vec<&ServerParams> = servers let imap_servers: Vec<&ServerParams> = servers
.iter() .iter()
.filter(|params| params.protocol == Protocol::Imap) .filter(|params| params.protocol == Protocol::Imap)
@@ -433,7 +433,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count 600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
); );
} }
let mut imap = match imap { let (mut imap, mut imap_session) = match imap {
Some(imap) => imap, Some(imap) => imap,
None => bail!(nicer_configuration_error(ctx, errors).await), None => bail!(nicer_configuration_error(ctx, errors).await),
}; };
@@ -454,11 +454,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let create_mvbox = ctx.should_watch_mvbox().await?; let create_mvbox = ctx.should_watch_mvbox().await?;
imap.configure_folders(ctx, create_mvbox).await?; imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
imap.session imap_session
.as_mut()
.context("no IMAP connection established")?
.select_with_uidvalidity(ctx, "INBOX") .select_with_uidvalidity(ctx, "INBOX")
.await .await
.context("could not read INBOX status")?; .context("could not read INBOX status")?;
@@ -579,7 +578,7 @@ async fn try_imap_one_param(
socks5_config: &Option<Socks5Config>, socks5_config: &Option<Socks5Config>,
addr: &str, addr: &str,
provider_strict_tls: bool, provider_strict_tls: bool,
) -> Result<Imap, ConfigurationError> { ) -> Result<(Imap, ImapSession), ConfigurationError> {
let inf = format!( let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}", "imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user, param.user,
@@ -617,9 +616,9 @@ async fn try_imap_one_param(
msg: format!("{err:#}"), msg: format!("{err:#}"),
}) })
} }
Ok(()) => { Ok(session) => {
info!(context, "success: {}", inf); info!(context, "success: {}", inf);
Ok(imap) Ok((imap, session))
} }
} }
} }

View File

@@ -461,13 +461,13 @@ impl Context {
// connection // connection
let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?; let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
connection.prepare(self).await?; let mut session = connection.prepare(self).await?;
// fetch imap folders // fetch imap folders
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] { for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
let (_, watch_folder) = convert_folder_meaning(self, folder_meaning).await?; let (_, watch_folder) = convert_folder_meaning(self, folder_meaning).await?;
connection connection
.fetch_move_delete(self, &watch_folder, folder_meaning) .fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
.await?; .await?;
} }
@@ -484,10 +484,8 @@ impl Context {
}; };
if quota_needs_update { if quota_needs_update {
if let Some(session) = connection.session.as_mut() { if let Err(err) = self.update_recent_quota(&mut session).await {
if let Err(err) = self.update_recent_quota(session).await { warn!(self, "Failed to update quota: {err:#}.");
warn!(self, "Failed to update quota: {err:#}.");
}
} }
} }

View File

@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use crate::config::Config; use crate::config::Config;
use crate::context::Context; use crate::context::Context;
use crate::imap::{Imap, ImapActionResult}; use crate::imap::{session::Session, ImapActionResult};
use crate::message::{Message, MsgId, Viewtype}; use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part}; use crate::mimeparser::{MimeMessage, Part};
use crate::tools::time; use crate::tools::time;
@@ -129,9 +129,11 @@ impl Message {
/// Actually download a message partially downloaded before. /// Actually download a message partially downloaded before.
/// ///
/// Most messages are downloaded automatically on fetch instead. /// Most messages are downloaded automatically on fetch instead.
pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Imap) -> Result<()> { pub(crate) async fn download_msg(
imap.prepare(context).await?; context: &Context,
msg_id: MsgId,
session: &mut Session,
) -> Result<()> {
let msg = Message::load_from_db(context, msg_id).await?; let msg = Message::load_from_db(context, msg_id).await?;
let row = context let row = context
.sql .sql
@@ -152,7 +154,7 @@ pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Im
return Err(anyhow!("Call download_full() again to try over.")); return Err(anyhow!("Call download_full() again to try over."));
}; };
match imap match session
.fetch_single_msg( .fetch_single_msg(
context, context,
&server_folder, &server_folder,
@@ -169,7 +171,7 @@ pub(crate) async fn download_msg(context: &Context, msg_id: MsgId, imap: &mut Im
} }
} }
impl Imap { impl Session {
/// Download a single message and pipe it to receive_imf(). /// Download a single message and pipe it to receive_imf().
/// ///
/// receive_imf() is not directly aware that this is a result of a call to download_msg(), /// receive_imf() is not directly aware that this is a result of a call to download_msg(),
@@ -194,10 +196,7 @@ impl Imap {
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new(); let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid); uid_message_ids.insert(uid, rfc724_mid);
let Some(session) = self.session.as_mut() else { let (last_uid, _received) = match self
return ImapActionResult::Failed;
};
let (last_uid, _received) = match session
.fetch_many_msgs( .fetch_many_msgs(
context, context,
folder, folder,

View File

@@ -69,10 +69,9 @@ const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])"; const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
#[derive(Debug)] #[derive(Debug)]
pub struct Imap { pub(crate) struct Imap {
pub(crate) idle_interrupt_receiver: Receiver<()>, pub(crate) idle_interrupt_receiver: Receiver<()>,
config: ImapConfig, config: ImapConfig,
pub(crate) session: Option<Session>,
login_failed_once: bool, login_failed_once: bool,
pub(crate) connectivity: ConnectivityStore, pub(crate) connectivity: ConnectivityStore,
@@ -255,7 +254,6 @@ impl Imap {
let imap = Imap { let imap = Imap {
idle_interrupt_receiver, idle_interrupt_receiver,
config, config,
session: None,
login_failed_once: false, login_failed_once: false,
connectivity: Default::default(), connectivity: Default::default(),
// 1 connection per minute + a burst of 2. // 1 connection per minute + a burst of 2.
@@ -298,15 +296,11 @@ impl Imap {
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`] /// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
/// instead if you are going to actually use connection rather than trying connection /// instead if you are going to actually use connection rather than trying connection
/// parameters. /// parameters.
pub async fn connect(&mut self, context: &Context) -> Result<()> { pub(crate) async fn connect(&mut self, context: &Context) -> Result<Session> {
if self.config.lp.server.is_empty() { if self.config.lp.server.is_empty() {
bail!("IMAP operation attempted while it is torn down"); bail!("IMAP operation attempted while it is torn down");
} }
if self.session.is_some() {
return Ok(());
}
let ratelimit_duration = self.ratelimit.read().await.until_can_send(); let ratelimit_duration = self.ratelimit.read().await.until_can_send();
if !ratelimit_duration.is_zero() { if !ratelimit_duration.is_zero() {
warn!( warn!(
@@ -410,7 +404,6 @@ impl Imap {
let mut lock = context.server_id.write().await; let mut lock = context.server_id.write().await;
*lock = session.capabilities.server_id.clone(); *lock = session.capabilities.server_id.clone();
self.session = Some(session);
self.login_failed_once = false; self.login_failed_once = false;
context.emit_event(EventType::ImapConnected(format!( context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}", "IMAP-LOGIN as {}",
@@ -418,7 +411,7 @@ impl Imap {
))); )));
self.connectivity.set_connected(context).await; self.connectivity.set_connected(context).await;
info!(context, "Successfully logged into IMAP server"); info!(context, "Successfully logged into IMAP server");
Ok(()) Ok(session)
} }
Err(err) => { Err(err) => {
@@ -461,22 +454,26 @@ impl Imap {
/// ///
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are /// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
/// determined. /// determined.
pub async fn prepare(&mut self, context: &Context) -> Result<()> { pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
if let Err(err) = self.connect(context).await { let mut session = match self.connect(context).await {
self.connectivity.set_err(context, &err).await; Ok(session) => session,
return Err(err); Err(err) => {
self.connectivity.set_err(context, &err).await;
return Err(err);
}
};
let folders_configured = context
.sql
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
.await?;
if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
let create_mvbox = true;
self.configure_folders(context, &mut session, create_mvbox)
.await?;
} }
self.ensure_configured_folders(context, true).await?; Ok(session)
Ok(())
}
/// Drops the session without disconnecting properly.
/// Useful in case of an IMAP error, when it's unclear if it's in a correct state and it's
/// easier to setup a new connection.
pub fn trigger_reconnect(&mut self, context: &Context) {
info!(context, "Dropping an IMAP connection.");
self.session = None;
} }
/// FETCH-MOVE-DELETE iteration. /// FETCH-MOVE-DELETE iteration.
@@ -486,6 +483,7 @@ impl Imap {
pub async fn fetch_move_delete( pub async fn fetch_move_delete(
&mut self, &mut self,
context: &Context, context: &Context,
session: &mut Session,
watch_folder: &str, watch_folder: &str,
folder_meaning: FolderMeaning, folder_meaning: FolderMeaning,
) -> Result<()> { ) -> Result<()> {
@@ -493,10 +491,9 @@ impl Imap {
// probably shutdown // probably shutdown
bail!("IMAP operation attempted while it is torn down"); bail!("IMAP operation attempted while it is torn down");
} }
self.prepare(context).await?;
let msgs_fetched = self let msgs_fetched = self
.fetch_new_messages(context, watch_folder, folder_meaning, false) .fetch_new_messages(context, session, watch_folder, folder_meaning, false)
.await .await
.context("fetch_new_messages")?; .context("fetch_new_messages")?;
if msgs_fetched && context.get_config_delete_device_after().await?.is_some() { if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
@@ -507,10 +504,6 @@ impl Imap {
context.scheduler.interrupt_ephemeral_task().await; context.scheduler.interrupt_ephemeral_task().await;
} }
let session = self
.session
.as_mut()
.context("no IMAP connection established")?;
session session
.move_delete_messages(context, watch_folder) .move_delete_messages(context, watch_folder)
.await .await
@@ -525,6 +518,7 @@ impl Imap {
pub(crate) async fn fetch_new_messages( pub(crate) async fn fetch_new_messages(
&mut self, &mut self,
context: &Context, context: &Context,
session: &mut Session,
folder: &str, folder: &str,
folder_meaning: FolderMeaning, folder_meaning: FolderMeaning,
fetch_existing_msgs: bool, fetch_existing_msgs: bool,
@@ -534,7 +528,6 @@ impl Imap {
return Ok(false); return Ok(false);
} }
let session = self.session.as_mut().context("No IMAP session")?;
let new_emails = session let new_emails = session
.select_with_uidvalidity(context, folder) .select_with_uidvalidity(context, folder)
.await .await
@@ -694,10 +687,7 @@ impl Imap {
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1)); uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
for (uid, fp) in uids_fetch { for (uid, fp) in uids_fetch {
if fp != fetch_partially { if fp != fetch_partially {
let (largest_uid_fetched_in_batch, received_msgs_in_batch) = self let (largest_uid_fetched_in_batch, received_msgs_in_batch) = session
.session
.as_mut()
.context("No IMAP session")?
.fetch_many_msgs( .fetch_many_msgs(
context, context,
folder, folder,
@@ -724,10 +714,7 @@ impl Imap {
// Largest known UID is normally less than UIDNEXT, // Largest known UID is normally less than UIDNEXT,
// but a message may have arrived between determining UIDNEXT // but a message may have arrived between determining UIDNEXT
// and executing the FETCH command. // and executing the FETCH command.
let mailbox_uid_next = self let mailbox_uid_next = session
.session
.as_ref()
.context("No IMAP session")?
.selected_mailbox .selected_mailbox
.as_ref() .as_ref()
.with_context(|| format!("Expected {folder:?} to be selected"))? .with_context(|| format!("Expected {folder:?} to be selected"))?
@@ -762,13 +749,15 @@ impl Imap {
/// ///
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server /// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
/// and show them in the chat list. /// and show them in the chat list.
pub(crate) async fn fetch_existing_msgs(&mut self, context: &Context) -> Result<()> { pub(crate) async fn fetch_existing_msgs(
&mut self,
context: &Context,
session: &mut Session,
) -> Result<()> {
if context.get_config_bool(Config::Bot).await? { if context.get_config_bool(Config::Bot).await? {
return Ok(()); // Bots don't want those messages return Ok(()); // Bots don't want those messages
} }
self.prepare(context).await.context("could not connect")?;
let session = self.session.as_mut().context("No IMAP session")?;
add_all_recipients_as_contacts(context, session, Config::ConfiguredSentboxFolder) add_all_recipients_as_contacts(context, session, Config::ConfiguredSentboxFolder)
.await .await
.context("failed to get recipients from the sentbox")?; .context("failed to get recipients from the sentbox")?;
@@ -794,7 +783,7 @@ impl Imap {
context, context,
"Fetching existing messages from folder {folder:?}." "Fetching existing messages from folder {folder:?}."
); );
self.fetch_new_messages(context, &folder, meaning, true) self.fetch_new_messages(context, session, &folder, meaning, true)
.await .await
.context("could not fetch existing messages")?; .context("could not fetch existing messages")?;
} }
@@ -1493,9 +1482,7 @@ impl Session {
} }
Ok(()) Ok(())
} }
}
impl Imap {
pub(crate) async fn prepare_imap_operation_on_msg( pub(crate) async fn prepare_imap_operation_on_msg(
&mut self, &mut self,
context: &Context, context: &Context,
@@ -1505,24 +1492,8 @@ impl Imap {
if uid == 0 { if uid == 0 {
return Some(ImapActionResult::RetryLater); return Some(ImapActionResult::RetryLater);
} }
if let Err(err) = self.prepare(context).await {
warn!(context, "prepare_imap_op failed: {}", err);
return Some(ImapActionResult::RetryLater);
}
let session = match self match self.select_folder(context, Some(folder)).await {
.session
.as_mut()
.context("no IMAP connection established")
{
Err(err) => {
error!(context, "Failed to prepare IMAP operation: {:#}", err);
return Some(ImapActionResult::Failed);
}
Ok(session) => session,
};
match session.select_folder(context, Some(folder)).await {
Ok(_) => None, Ok(_) => None,
Err(select_folder::Error::ConnectionLost) => { Err(select_folder::Error::ConnectionLost) => {
warn!(context, "Lost imap connection"); warn!(context, "Lost imap connection");
@@ -1539,25 +1510,6 @@ impl Imap {
} }
} }
pub async fn ensure_configured_folders(
&mut self,
context: &Context,
create_mvbox: bool,
) -> Result<()> {
let folders_configured = context
.sql
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
.await?;
if folders_configured.unwrap_or_default() >= constants::DC_FOLDERS_CONFIGURED_VERSION {
return Ok(());
}
if let Err(err) = self.connect(context).await {
self.connectivity.set_err(context, &err).await;
return Err(err);
}
self.configure_folders(context, create_mvbox).await
}
/// Attempts to configure mvbox. /// Attempts to configure mvbox.
/// ///
/// Tries to find any folder in the given list of `folders`. If none is found, tries to create /// Tries to find any folder in the given list of `folders`. If none is found, tries to create
@@ -1572,27 +1524,22 @@ impl Imap {
folders: &[&'a str], folders: &[&'a str],
create_mvbox: bool, create_mvbox: bool,
) -> Result<Option<&'a str>> { ) -> Result<Option<&'a str>> {
let session = self
.session
.as_mut()
.context("no IMAP connection established")?;
// Close currently selected folder if needed. // Close currently selected folder if needed.
// We are going to select folders using low-level EXAMINE operations below. // We are going to select folders using low-level EXAMINE operations below.
session.select_folder(context, None).await?; self.select_folder(context, None).await?;
for folder in folders { for folder in folders {
info!(context, "Looking for MVBOX-folder \"{}\"...", &folder); info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
let res = session.examine(&folder).await; let res = self.examine(&folder).await;
if res.is_ok() { if res.is_ok() {
info!( info!(
context, context,
"MVBOX-folder {:?} successfully selected, using it.", &folder "MVBOX-folder {:?} successfully selected, using it.", &folder
); );
session.close().await?; self.close().await?;
// Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise // Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise
// emails moved before that wouldn't be fetched but considered "old" instead. // emails moved before that wouldn't be fetched but considered "old" instead.
session.select_with_uidvalidity(context, folder).await?; self.select_with_uidvalidity(context, folder).await?;
return Ok(Some(folder)); return Ok(Some(folder));
} }
} }
@@ -1603,7 +1550,7 @@ impl Imap {
let Some(folder) = folders.first() else { let Some(folder) = folders.first() else {
return Ok(None); return Ok(None);
}; };
match session.select_with_uidvalidity(context, folder).await { match self.select_with_uidvalidity(context, folder).await {
Ok(_) => { Ok(_) => {
info!(context, "MVBOX-folder {} created.", folder); info!(context, "MVBOX-folder {} created.", folder);
return Ok(Some(folder)); return Ok(Some(folder));
@@ -1614,13 +1561,15 @@ impl Imap {
} }
Ok(None) Ok(None)
} }
}
pub async fn configure_folders(&mut self, context: &Context, create_mvbox: bool) -> Result<()> { impl Imap {
let session = self pub(crate) async fn configure_folders(
.session &mut self,
.as_mut() context: &Context,
.context("no IMAP connection established")?; session: &mut Session,
create_mvbox: bool,
) -> Result<()> {
let mut folders = session let mut folders = session
.list(Some(""), Some("*")) .list(Some(""), Some("*"))
.await .await
@@ -1657,7 +1606,7 @@ impl Imap {
info!(context, "Using \"{}\" as folder-delimiter.", delimiter); info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
let fallback_folder = format!("INBOX{delimiter}DeltaChat"); let fallback_folder = format!("INBOX{delimiter}DeltaChat");
let mvbox_folder = self let mvbox_folder = session
.configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox) .configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
.await .await
.context("failed to configure mvbox")?; .context("failed to configure mvbox")?;

View File

@@ -4,13 +4,12 @@ use anyhow::{bail, Context as _, Result};
use async_channel::Receiver; use async_channel::Receiver;
use async_imap::extensions::idle::IdleResponse; use async_imap::extensions::idle::IdleResponse;
use futures_lite::FutureExt; use futures_lite::FutureExt;
use tokio::time::timeout;
use super::session::Session; use super::session::Session;
use super::Imap; use super::Imap;
use crate::config::Config;
use crate::context::Context; use crate::context::Context;
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning}; use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
use crate::log::LogExt;
use crate::tools::{self, time_elapsed}; use crate::tools::{self, time_elapsed};
/// Timeout after which IDLE is finished /// Timeout after which IDLE is finished
@@ -98,90 +97,38 @@ impl Session {
} }
impl Imap { impl Imap {
/// Idle using polling.
pub(crate) async fn fake_idle( pub(crate) async fn fake_idle(
&mut self, &mut self,
context: &Context, context: &Context,
session: &mut Session,
watch_folder: String, watch_folder: String,
folder_meaning: FolderMeaning, folder_meaning: FolderMeaning,
) { ) -> Result<()> {
// Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
let fake_idle_start_time = tools::Time::now(); let fake_idle_start_time = tools::Time::now();
// Do not poll, just wait for an interrupt when no folder is passed in.
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder); info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
const TIMEOUT_INIT_MS: u64 = 60_000; // Loop until we are interrupted or until we fetch something.
let mut timeout_ms: u64 = TIMEOUT_INIT_MS;
enum Event {
Tick,
Interrupt,
}
// loop until we are interrupted or if we fetched something
loop { loop {
use futures::future::FutureExt; match timeout(Duration::from_secs(60), self.idle_interrupt_receiver.recv()).await {
use rand::Rng; Err(_) => {
// Let's see if fetching messages results
let mut interval = tokio::time::interval(Duration::from_millis(timeout_ms));
timeout_ms = timeout_ms
.saturating_add(rand::thread_rng().gen_range((timeout_ms / 2)..=timeout_ms));
interval.tick().await; // The first tick completes immediately.
match interval
.tick()
.map(|_| Event::Tick)
.race(
self.idle_interrupt_receiver
.recv()
.map(|_| Event::Interrupt),
)
.await
{
Event::Tick => {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.prepare(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if let Some(session) = &self.session {
if session.can_idle()
&& !context
.get_config_bool(Config::DisableIdle)
.await
.context("Failed to get disable_idle config")
.log_err(context)
.unwrap_or_default()
{
// we only fake-idled because network was gone during IDLE, probably
break;
}
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but // in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch // will have already fetched the messages so perform_*_fetch
// will not find any new. // will not find any new.
match self let res = self
.fetch_new_messages(context, &watch_folder, folder_meaning, false) .fetch_new_messages(context, session, &watch_folder, folder_meaning, false)
.await .await?;
{
Ok(res) => { info!(context, "fetch_new_messages returned {:?}", res);
info!(context, "fetch_new_messages returned {:?}", res);
timeout_ms = TIMEOUT_INIT_MS; if res {
if res { break;
break;
}
}
Err(err) => {
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect(context);
}
} }
} }
Event::Interrupt => { Ok(_) => {
info!(context, "Fake IDLE interrupted"); info!(context, "Fake IDLE interrupted.");
break; break;
} }
} }
@@ -192,5 +139,6 @@ impl Imap {
"IMAP-fake-IDLE done after {:.4}s", "IMAP-fake-IDLE done after {:.4}s",
time_elapsed(&fake_idle_start_time).as_millis() as f64 / 1000., time_elapsed(&fake_idle_start_time).as_millis() as f64 / 1000.,
); );
Ok(())
} }
} }

View File

@@ -4,14 +4,18 @@ use anyhow::{Context as _, Result};
use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name}; use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
use crate::config::Config; use crate::config::Config;
use crate::imap::Imap; use crate::imap::{session::Session, Imap};
use crate::log::LogExt; use crate::log::LogExt;
use crate::tools::{self, time_elapsed}; use crate::tools::{self, time_elapsed};
use crate::{context::Context, imap::FolderMeaning}; use crate::{context::Context, imap::FolderMeaning};
impl Imap { impl Imap {
/// Returns true if folders were scanned, false if scanning was postponed. /// Returns true if folders were scanned, false if scanning was postponed.
pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<bool> { pub(crate) async fn scan_folders(
&mut self,
context: &Context,
session: &mut Session,
) -> Result<bool> {
// First of all, debounce to once per minute: // First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await; let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan { if let Some(last_scan) = *last_scan {
@@ -26,8 +30,6 @@ impl Imap {
} }
info!(context, "Starting full folder scan"); info!(context, "Starting full folder scan");
self.prepare(context).await?;
let session = self.session.as_mut().context("No IMAP session")?;
let folders = session.list_folders().await?; let folders = session.list_folders().await?;
let watched_folders = get_watched_folders(context).await?; let watched_folders = get_watched_folders(context).await?;
@@ -64,18 +66,16 @@ impl Imap {
&& folder_meaning != FolderMeaning::Drafts && folder_meaning != FolderMeaning::Drafts
&& folder_meaning != FolderMeaning::Trash && folder_meaning != FolderMeaning::Trash
{ {
let session = self.session.as_mut().context("no session")?;
// Drain leftover unsolicited EXISTS messages // Drain leftover unsolicited EXISTS messages
session.server_sent_unsolicited_exists(context)?; session.server_sent_unsolicited_exists(context)?;
loop { loop {
self.fetch_move_delete(context, folder.name(), folder_meaning) self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await .await
.context("Can't fetch new msgs in scanned folder") .context("Can't fetch new msgs in scanned folder")
.log_err(context) .log_err(context)
.ok(); .ok();
let session = self.session.as_mut().context("no session")?;
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again // If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !session.server_sent_unsolicited_exists(context)? { if !session.server_sent_unsolicited_exists(context)? {
break; break;

View File

@@ -19,7 +19,7 @@ use crate::context::Context;
use crate::download::{download_msg, DownloadState}; use crate::download::{download_msg, DownloadState};
use crate::ephemeral::{self, delete_expired_imap_messages}; use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::events::EventType; use crate::events::EventType;
use crate::imap::{FolderMeaning, Imap}; use crate::imap::{session::Session, FolderMeaning, Imap};
use crate::location; use crate::location;
use crate::log::LogExt; use crate::log::LogExt;
use crate::message::MsgId; use crate::message::MsgId;
@@ -330,7 +330,7 @@ pub(crate) struct Scheduler {
recently_seen_loop: RecentlySeenLoop, recently_seen_loop: RecentlySeenLoop,
} }
async fn download_msgs(context: &Context, imap: &mut Imap) -> Result<()> { async fn download_msgs(context: &Context, session: &mut Session) -> Result<()> {
let msg_ids = context let msg_ids = context
.sql .sql
.query_map( .query_map(
@@ -349,7 +349,7 @@ async fn download_msgs(context: &Context, imap: &mut Imap) -> Result<()> {
.await?; .await?;
for msg_id in msg_ids { for msg_id in msg_ids {
if let Err(err) = download_msg(context, msg_id, imap).await { if let Err(err) = download_msg(context, msg_id, session).await {
warn!(context, "Failed to download message {msg_id}: {:#}.", err); warn!(context, "Failed to download message {msg_id}: {:#}.", err);
// Update download state to failure // Update download state to failure
@@ -392,94 +392,26 @@ async fn inbox_loop(
return; return;
}; };
let mut old_session: Option<Session> = None;
loop { loop {
if let Err(err) = connection.prepare(&ctx).await { let session = if let Some(session) = old_session.take() {
warn!(ctx, "Failed to prepare connection: {:#}.", err); session
} } else {
match connection.prepare(&ctx).await {
{ Err(err) => {
// Update quota no more than once a minute. warn!(ctx, "Failed to prepare connection: {:#}.", err);
let quota_needs_update = { continue;
let quota = ctx.quota.read().await;
quota
.as_ref()
.filter(|quota| time_elapsed(&quota.modified) > Duration::from_secs(60))
.is_none()
};
if quota_needs_update {
if let Some(session) = connection.session.as_mut() {
if let Err(err) = ctx.update_recent_quota(session).await {
warn!(ctx, "Failed to update quota: {:#}.", err);
}
} }
} Ok(session) => session,
}
let resync_requested = ctx.resync_request.swap(false, Ordering::Relaxed);
if resync_requested {
if let Some(session) = connection.session.as_mut() {
if let Err(err) = session.resync_folders(&ctx).await {
warn!(ctx, "Failed to resync folders: {:#}.", err);
ctx.resync_request.store(true, Ordering::Relaxed);
}
}
}
maybe_add_time_based_warnings(&ctx).await;
match ctx.get_config_i64(Config::LastHousekeeping).await {
Ok(last_housekeeping_time) => {
let next_housekeeping_time =
last_housekeeping_time.saturating_add(60 * 60 * 24);
if next_housekeeping_time <= time() {
sql::housekeeping(&ctx).await.log_err(&ctx).ok();
}
}
Err(err) => {
warn!(ctx, "Failed to get last housekeeping time: {}", err);
} }
}; };
match ctx.get_config_bool(Config::FetchedExistingMsgs).await { match inbox_fetch_idle(&ctx, &mut connection, session).await {
Ok(fetched_existing_msgs) => { Err(err) => warn!(ctx, "Failed fetch_idle: {err:#}"),
if !fetched_existing_msgs { Ok(session) => {
// Consider it done even if we fail. old_session = Some(session);
//
// This operation is not critical enough to retry,
// especially if the error is persistent.
if let Err(err) = ctx
.set_config_internal(
Config::FetchedExistingMsgs,
config::from_bool(true),
)
.await
{
warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err);
}
if let Err(err) = connection.fetch_existing_msgs(&ctx).await {
warn!(ctx, "Failed to fetch existing messages: {:#}", err);
connection.trigger_reconnect(&ctx);
}
}
}
Err(err) => {
warn!(ctx, "Can't get Config::FetchedExistingMsgs: {:#}", err);
} }
} }
if let Err(err) = download_msgs(&ctx, &mut connection).await {
warn!(ctx, "Failed to download messages: {:#}", err);
}
if let Some(session) = connection.session.as_mut() {
if let Err(err) = session.fetch_metadata(&ctx).await {
warn!(ctx, "Failed to fetch metadata: {err:#}.");
}
}
fetch_idle(&ctx, &mut connection, FolderMeaning::Inbox).await;
} }
}; };
@@ -525,82 +457,123 @@ pub async fn convert_folder_meaning(
Ok((folder_config, watch_folder)) Ok((folder_config, watch_folder))
} }
/// Implement a single iteration of IMAP loop. async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result<Session> {
/// // Update quota no more than once a minute.
/// This function performs all IMAP operations on a single folder, selecting it if necessary and let quota_needs_update = {
/// handling all the errors. In case of an error, it is logged, but not propagated upwards. If let quota = ctx.quota.read().await;
/// critical operation fails such as fetching new messages fails, connection is reset via quota
/// `trigger_reconnect`, so a fresh one can be opened. .as_ref()
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: FolderMeaning) { .filter(|quota| time_elapsed(&quota.modified) > Duration::from_secs(60))
let create_mvbox = true; .is_none()
if let Err(err) = connection };
.ensure_configured_folders(ctx, create_mvbox) if quota_needs_update {
.await if let Err(err) = ctx.update_recent_quota(&mut session).await {
{ warn!(ctx, "Failed to update quota: {:#}.", err);
warn!( }
ctx,
"Cannot watch {folder_meaning}, ensure_configured_folders() failed: {:#}", err,
);
connection.idle_interrupt_receiver.recv().await.ok();
return;
} }
let (folder_config, watch_folder) = match convert_folder_meaning(ctx, folder_meaning).await {
Ok(meaning) => meaning, let resync_requested = ctx.resync_request.swap(false, Ordering::Relaxed);
Err(error) => { if resync_requested {
// Warning instead of error because the folder may not be configured. if let Err(err) = session.resync_folders(ctx).await {
// For example, this happens if the server does not have Sent folder warn!(ctx, "Failed to resync folders: {:#}.", err);
// but watching Sent folder is enabled. ctx.resync_request.store(true, Ordering::Relaxed);
warn!(ctx, "Error converting IMAP Folder name: {:?}", error); }
connection.connectivity.set_not_configured(ctx).await; }
connection.idle_interrupt_receiver.recv().await.ok();
return; maybe_add_time_based_warnings(ctx).await;
match ctx.get_config_i64(Config::LastHousekeeping).await {
Ok(last_housekeeping_time) => {
let next_housekeeping_time = last_housekeeping_time.saturating_add(60 * 60 * 24);
if next_housekeeping_time <= time() {
sql::housekeeping(ctx).await.log_err(ctx).ok();
}
}
Err(err) => {
warn!(ctx, "Failed to get last housekeeping time: {}", err);
} }
}; };
// connect and fake idle if unable to connect match ctx.get_config_bool(Config::FetchedExistingMsgs).await {
if let Err(err) = connection Ok(fetched_existing_msgs) => {
.prepare(ctx) if !fetched_existing_msgs {
.await // Consider it done even if we fail.
.context("prepare IMAP connection") //
{ // This operation is not critical enough to retry,
warn!(ctx, "{:#}", err); // especially if the error is persistent.
connection.trigger_reconnect(ctx); if let Err(err) = ctx
return; .set_config_internal(Config::FetchedExistingMsgs, config::from_bool(true))
} .await
{
warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err);
}
if folder_config == Config::ConfiguredInboxFolder { if let Err(err) = imap.fetch_existing_msgs(ctx, &mut session).await {
if let Some(session) = connection.session.as_mut() { warn!(ctx, "Failed to fetch existing messages: {:#}", err);
session }
.store_seen_flags_on_imap(ctx) }
.await }
.context("store_seen_flags_on_imap") Err(err) => {
.log_err(ctx) warn!(ctx, "Can't get Config::FetchedExistingMsgs: {:#}", err);
.ok();
} else {
warn!(ctx, "No session even though we just prepared it");
} }
} }
// Fetch the watched folder. download_msgs(ctx, &mut session)
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, folder_meaning)
.await .await
.context("fetch_move_delete") .context("Failed to download messages")?;
{ session
connection.trigger_reconnect(ctx); .fetch_metadata(ctx)
warn!(ctx, "{:#}", err); .await
return; .context("Failed to fetch metadata")?;
let session = fetch_idle(ctx, imap, session, FolderMeaning::Inbox).await?;
Ok(session)
}
/// Implement a single iteration of IMAP loop.
///
/// This function performs all IMAP operations on a single folder, selecting it if necessary and
/// handling all the errors. In case of an error, an error is returned and connection is dropped,
/// otherwise connection is returned.
async fn fetch_idle(
ctx: &Context,
connection: &mut Imap,
mut session: Session,
folder_meaning: FolderMeaning,
) -> Result<Session> {
let (folder_config, watch_folder) = match convert_folder_meaning(ctx, folder_meaning).await {
Ok(meaning) => meaning,
Err(err) => {
// Warning instead of error because the folder may not be configured.
// For example, this happens if the server does not have Sent folder
// but watching Sent folder is enabled.
warn!(ctx, "Error converting IMAP Folder name: {err:#}.");
connection.connectivity.set_not_configured(ctx).await;
connection.idle_interrupt_receiver.recv().await.ok();
return Err(err);
}
};
if folder_config == Config::ConfiguredInboxFolder {
session
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap")?;
} }
// Fetch the watched folder.
connection
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
.await
.context("fetch_move_delete")?;
// Mark expired messages for deletion. Marked messages will be deleted from the server // Mark expired messages for deletion. Marked messages will be deleted from the server
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not // on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would // called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching. // otherwise slow down message fetching.
delete_expired_imap_messages(ctx) delete_expired_imap_messages(ctx)
.await .await
.context("delete_expired_imap_messages") .context("delete_expired_imap_messages")?;
.log_err(ctx)
.ok();
// Scan additional folders only after finishing fetching the watched folder. // Scan additional folders only after finishing fetching the watched folder.
// //
@@ -608,7 +581,11 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
// be able to scan all folders before time is up if there are many of them. // be able to scan all folders before time is up if there are many of them.
if folder_config == Config::ConfiguredInboxFolder { if folder_config == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages // Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection.scan_folders(ctx).await.context("scan_folders") { match connection
.scan_folders(ctx, &mut session)
.await
.context("scan_folders")
{
Err(err) => { Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing // Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something // but maybe just one folder can't be selected or something
@@ -621,42 +598,26 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
// In most cases this will select the watched folder and return because there are // In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE // no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip. // there, so this does not take additional protocol round-trip.
if let Err(err) = connection connection
.fetch_move_delete(ctx, &watch_folder, folder_meaning) .fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning)
.await .await
.context("fetch_move_delete after scan_folders") .context("fetch_move_delete after scan_folders")?;
{
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
return;
}
} }
Ok(false) => {} Ok(false) => {}
} }
} }
// Synchronize Seen flags. // Synchronize Seen flags.
if let Some(session) = connection.session.as_mut() { session
session .sync_seen_flags(ctx, &watch_folder)
.sync_seen_flags(ctx, &watch_folder) .await
.await .context("sync_seen_flags")
.context("sync_seen_flags") .log_err(ctx)
.log_err(ctx) .ok();
.ok();
} else {
warn!(ctx, "No IMAP session, skipping flag synchronization.");
}
connection.connectivity.set_idle(ctx).await; connection.connectivity.set_idle(ctx).await;
ctx.emit_event(EventType::ImapInboxIdle); ctx.emit_event(EventType::ImapInboxIdle);
let Some(session) = connection.session.take() else {
warn!(ctx, "No IMAP session, going to fake idle.");
connection
.fake_idle(ctx, watch_folder, folder_meaning)
.await;
return;
};
if !session.can_idle() { if !session.can_idle() {
info!( info!(
@@ -664,9 +625,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
"IMAP session does not support IDLE, going to fake idle." "IMAP session does not support IDLE, going to fake idle."
); );
connection connection
.fake_idle(ctx, watch_folder, folder_meaning) .fake_idle(ctx, &mut session, watch_folder, folder_meaning)
.await; .await?;
return; return Ok(session);
} }
if ctx if ctx
@@ -678,29 +639,22 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
{ {
info!(ctx, "IMAP IDLE is disabled, going to fake idle."); info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
connection connection
.fake_idle(ctx, watch_folder, folder_meaning) .fake_idle(ctx, &mut session, watch_folder, folder_meaning)
.await; .await?;
return; return Ok(session);
} }
info!(ctx, "IMAP session supports IDLE, using it."); info!(ctx, "IMAP session supports IDLE, using it.");
match session let session = session
.idle( .idle(
ctx, ctx,
connection.idle_interrupt_receiver.clone(), connection.idle_interrupt_receiver.clone(),
&watch_folder, &watch_folder,
) )
.await .await
.context("idle") .context("idle")?;
{
Ok(session) => { Ok(session)
connection.session = Some(session);
}
Err(err) => {
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
}
}
} }
async fn simple_imap_loop( async fn simple_imap_loop(
@@ -726,8 +680,26 @@ async fn simple_imap_loop(
return; return;
} }
let mut old_session: Option<Session> = None;
loop { loop {
fetch_idle(&ctx, &mut connection, folder_meaning).await; let session = if let Some(session) = old_session.take() {
session
} else {
match connection.prepare(&ctx).await {
Err(err) => {
warn!(ctx, "Failed to prepare connection: {:#}.", err);
continue;
}
Ok(session) => session,
}
};
match fetch_idle(&ctx, &mut connection, session, folder_meaning).await {
Err(err) => warn!(ctx, "Failed fetch_idle: {err:#}"),
Ok(session) => {
old_session = Some(session);
}
}
} }
}; };