deltachat/imap/
scan_folders.rs

1use std::collections::BTreeMap;
2
3use anyhow::{Context as _, Result};
4
5use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
6use crate::config::Config;
7use crate::imap::{session::Session, Imap};
8use crate::log::LogExt;
9use crate::tools::{self, time_elapsed};
10use crate::{context::Context, imap::FolderMeaning};
11
12impl Imap {
13    /// Returns true if folders were scanned, false if scanning was postponed.
14    pub(crate) async fn scan_folders(
15        &mut self,
16        context: &Context,
17        session: &mut Session,
18    ) -> Result<bool> {
19        // First of all, debounce to once per minute:
20        {
21            let mut last_scan = context.last_full_folder_scan.lock().await;
22            if let Some(last_scan) = *last_scan {
23                let elapsed_secs = time_elapsed(&last_scan).as_secs();
24                let debounce_secs = context
25                    .get_config_u64(Config::ScanAllFoldersDebounceSecs)
26                    .await?;
27
28                if elapsed_secs < debounce_secs {
29                    return Ok(false);
30                }
31            }
32
33            // Update the timestamp before scanning the folders
34            // to avoid holding the lock for too long.
35            // This means next scan is delayed even if
36            // the current one fails.
37            last_scan.replace(tools::Time::now());
38        }
39        info!(context, "Starting full folder scan");
40
41        let folders = session.list_folders().await?;
42        let watched_folders = get_watched_folders(context).await?;
43
44        let mut folder_configs = BTreeMap::new();
45        let mut folder_names = Vec::new();
46
47        for folder in folders {
48            let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
49            if folder_meaning == FolderMeaning::Virtual {
50                // Gmail has virtual folders that should be skipped. For example,
51                // emails appear in the inbox and under "All Mail" as soon as it is
52                // received. The code used to wrongly conclude that the email had
53                // already been moved and left it in the inbox.
54                continue;
55            }
56            folder_names.push(folder.name().to_string());
57            let folder_name_meaning = get_folder_meaning_by_name(folder.name());
58
59            if let Some(config) = folder_meaning.to_config() {
60                // Always takes precedence
61                folder_configs.insert(config, folder.name().to_string());
62            } else if let Some(config) = folder_name_meaning.to_config() {
63                // only set if none has been already set
64                folder_configs
65                    .entry(config)
66                    .or_insert_with(|| folder.name().to_string());
67            }
68
69            let folder_meaning = match folder_meaning {
70                FolderMeaning::Unknown => folder_name_meaning,
71                _ => folder_meaning,
72            };
73
74            // Don't scan folders that are watched anyway
75            if !watched_folders.contains(&folder.name().to_string())
76                && folder_meaning != FolderMeaning::Drafts
77                && folder_meaning != FolderMeaning::Trash
78            {
79                self.fetch_move_delete(context, session, folder.name(), folder_meaning)
80                    .await
81                    .context("Can't fetch new msgs in scanned folder")
82                    .log_err(context)
83                    .ok();
84            }
85        }
86
87        // Set configs for necessary folders. Or reset if the folder was deleted.
88        for conf in [
89            Config::ConfiguredSentboxFolder,
90            Config::ConfiguredTrashFolder,
91        ] {
92            let val = folder_configs.get(&conf).map(|s| s.as_str());
93            let interrupt = conf == Config::ConfiguredTrashFolder
94                && val.is_some()
95                && context.get_config(conf).await?.is_none();
96            context.set_config_internal(conf, val).await?;
97            if interrupt {
98                // `Imap::fetch_move_delete()` is possible now for other folders (NB: we are in the
99                // Inbox loop).
100                context.scheduler.interrupt_oboxes().await;
101            }
102        }
103
104        info!(context, "Found folders: {folder_names:?}.");
105        Ok(true)
106    }
107}
108
109pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
110    let mut res = vec![Config::ConfiguredInboxFolder];
111    if context.get_config_bool(Config::SentboxWatch).await? {
112        res.push(Config::ConfiguredSentboxFolder);
113    }
114    if context.should_watch_mvbox().await? {
115        res.push(Config::ConfiguredMvboxFolder);
116    }
117    Ok(res)
118}
119
120pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
121    let mut res = Vec::new();
122    for folder_config in get_watched_folder_configs(context).await? {
123        if let Some(folder) = context.get_config(folder_config).await? {
124            res.push(folder);
125        }
126    }
127    Ok(res)
128}