1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use std::collections::BTreeMap;

use anyhow::{Context as _, Result};

use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
use crate::config::Config;
use crate::imap::{session::Session, Imap};
use crate::log::LogExt;
use crate::tools::{self, time_elapsed};
use crate::{context::Context, imap::FolderMeaning};

impl Imap {
    /// Returns true if folders were scanned, false if scanning was postponed.
    pub(crate) async fn scan_folders(
        &mut self,
        context: &Context,
        session: &mut Session,
    ) -> Result<bool> {
        // First of all, debounce to once per minute:
        let mut last_scan = context.last_full_folder_scan.lock().await;
        if let Some(last_scan) = *last_scan {
            let elapsed_secs = time_elapsed(&last_scan).as_secs();
            let debounce_secs = context
                .get_config_u64(Config::ScanAllFoldersDebounceSecs)
                .await?;

            if elapsed_secs < debounce_secs {
                return Ok(false);
            }
        }
        info!(context, "Starting full folder scan");

        let folders = session.list_folders().await?;
        let watched_folders = get_watched_folders(context).await?;

        let mut folder_configs = BTreeMap::new();

        for folder in folders {
            let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
            if folder_meaning == FolderMeaning::Virtual {
                // Gmail has virtual folders that should be skipped. For example,
                // emails appear in the inbox and under "All Mail" as soon as it is
                // received. The code used to wrongly conclude that the email had
                // already been moved and left it in the inbox.
                continue;
            }
            let folder_name_meaning = get_folder_meaning_by_name(folder.name());

            if let Some(config) = folder_meaning.to_config() {
                // Always takes precedence
                folder_configs.insert(config, folder.name().to_string());
            } else if let Some(config) = folder_name_meaning.to_config() {
                // only set if none has been already set
                folder_configs
                    .entry(config)
                    .or_insert_with(|| folder.name().to_string());
            }

            let folder_meaning = match folder_meaning {
                FolderMeaning::Unknown => folder_name_meaning,
                _ => folder_meaning,
            };

            // Don't scan folders that are watched anyway
            if !watched_folders.contains(&folder.name().to_string())
                && folder_meaning != FolderMeaning::Drafts
                && folder_meaning != FolderMeaning::Trash
            {
                // Drain leftover unsolicited EXISTS messages
                session.server_sent_unsolicited_exists(context)?;

                loop {
                    self.fetch_move_delete(context, session, folder.name(), folder_meaning)
                        .await
                        .context("Can't fetch new msgs in scanned folder")
                        .log_err(context)
                        .ok();

                    // If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
                    if !session.server_sent_unsolicited_exists(context)? {
                        break;
                    }
                }
            }
        }

        // Set configs for necessary folders. Or reset if the folder was deleted.
        for conf in [
            Config::ConfiguredSentboxFolder,
            Config::ConfiguredTrashFolder,
        ] {
            let val = folder_configs.get(&conf).map(|s| s.as_str());
            let interrupt = conf == Config::ConfiguredTrashFolder
                && val.is_some()
                && context.get_config(conf).await?.is_none();
            context.set_config_internal(conf, val).await?;
            if interrupt {
                // `Imap::fetch_move_delete()` is possible now for other folders (NB: we are in the
                // Inbox loop).
                context.scheduler.interrupt_oboxes().await;
            }
        }

        last_scan.replace(tools::Time::now());
        Ok(true)
    }
}

pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
    let mut res = vec![Config::ConfiguredInboxFolder];
    if context.get_config_bool(Config::SentboxWatch).await? {
        res.push(Config::ConfiguredSentboxFolder);
    }
    if context.should_watch_mvbox().await? {
        res.push(Config::ConfiguredMvboxFolder);
    }
    Ok(res)
}

pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
    let mut res = Vec::new();
    for folder_config in get_watched_folder_configs(context).await? {
        if let Some(folder) = context.get_config(folder_config).await? {
            res.push(folder);
        }
    }
    Ok(res)
}