Delete messages to the Trash folder for Gmail by default (#3957)

Gmail archives messages marked as `\Deleted` by default if those messages aren't in the Trash. But
if move them to the Trash instead, they will be auto-deleted in 30 days.
This commit is contained in:
iequidoo
2023-01-18 13:35:19 -03:00
committed by iequidoo
parent 4790ad0478
commit 604c4fcb71
17 changed files with 483 additions and 310 deletions

View File

@@ -9,6 +9,8 @@
### Fixes ### Fixes
- Start SQL transactions with IMMEDIATE behaviour rather than default DEFERRED one. #4063 - Start SQL transactions with IMMEDIATE behaviour rather than default DEFERRED one. #4063
- Fix a problem with Gmail where (auto-)deleted messages would get archived instead of deleted.
Move them to the Trash folder for Gmail which auto-deletes trashed messages in 30 days #3972
### API-Changes ### API-Changes

View File

@@ -1954,6 +1954,7 @@ def test_immediate_autodelete(acfactory, lp):
assert msg.text == "hello" assert msg.text == "hello"
lp.sec("ac2: wait for close/expunge on autodelete") lp.sec("ac2: wait for close/expunge on autodelete")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded") ac2._evtracker.get_info_contains("close/expunge succeeded")
lp.sec("ac2: check that message was autodeleted on server") lp.sec("ac2: check that message was autodeleted on server")
@@ -1995,6 +1996,34 @@ def test_delete_multiple_messages(acfactory, lp):
assert len(ac2.direct_imap.get_all_messages()) == 1 assert len(ac2.direct_imap.get_all_messages()) == 1
def test_trash_multiple_messages(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.set_config("delete_to_trash", "1")
chat12 = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: sending 3 messages")
texts = ["first", "second", "third"]
for text in texts:
chat12.send_text(text)
lp.sec("ac2: waiting for all messages on the other side")
to_delete = []
for text in texts:
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text in texts
if text != "second":
to_delete.append(msg)
lp.sec("ac2: deleting all messages except second")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
lp.sec("ac2: test that only one message is left")
ac2.direct_imap.select_config_folder("inbox")
assert len(ac2.direct_imap.get_all_messages()) == 1
def test_configure_error_msgs_wrong_pw(acfactory): def test_configure_error_msgs_wrong_pw(acfactory):
configdict = acfactory.get_next_liveconfig() configdict = acfactory.get_next_liveconfig()
ac1 = acfactory.get_unconfigured_account() ac1 = acfactory.get_unconfigured_account()

View File

@@ -1,5 +1,7 @@
//! # Key-value configuration management. //! # Key-value configuration management.
use std::str::FromStr;
use anyhow::{ensure, Context as _, Result}; use anyhow::{ensure, Context as _, Result};
use strum::{EnumProperty, IntoEnumIterator}; use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString}; use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -173,6 +175,10 @@ pub enum Config {
#[strum(props(default = "0"))] #[strum(props(default = "0"))]
DeleteDeviceAfter, DeleteDeviceAfter,
/// Move messages to the Trash folder instead of marking them "\Deleted". Overrides
/// `ProviderOptions::delete_to_trash`.
DeleteToTrash,
/// Save raw MIME messages with headers in the database if true. /// Save raw MIME messages with headers in the database if true.
SaveMimeHeaders, SaveMimeHeaders,
@@ -227,6 +233,9 @@ pub enum Config {
/// Configured "Sent" folder. /// Configured "Sent" folder.
ConfiguredSentboxFolder, ConfiguredSentboxFolder,
/// Configured "Trash" folder.
ConfiguredTrashFolder,
/// Unix timestamp of the last successful configuration. /// Unix timestamp of the last successful configuration.
ConfiguredTimestamp, ConfiguredTimestamp,
@@ -327,30 +336,37 @@ impl Context {
} }
} }
/// Returns 32-bit signed integer configuration value for the given key. /// Returns Some(T) if a value for the given key exists and was successfully parsed.
pub async fn get_config_int(&self, key: Config) -> Result<i32> { /// Returns None if could not parse.
pub async fn get_config_parsed<T: FromStr>(&self, key: Config) -> Result<Option<T>> {
self.get_config(key) self.get_config(key)
.await .await
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default()) .map(|s: Option<String>| s.and_then(|s| s.parse().ok()))
}
/// Returns 32-bit signed integer configuration value for the given key.
pub async fn get_config_int(&self, key: Config) -> Result<i32> {
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
} }
/// Returns 64-bit signed integer configuration value for the given key. /// Returns 64-bit signed integer configuration value for the given key.
pub async fn get_config_i64(&self, key: Config) -> Result<i64> { pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
self.get_config(key) Ok(self.get_config_parsed(key).await?.unwrap_or_default())
.await
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
} }
/// Returns 64-bit unsigned integer configuration value for the given key. /// Returns 64-bit unsigned integer configuration value for the given key.
pub async fn get_config_u64(&self, key: Config) -> Result<u64> { pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
self.get_config(key) Ok(self.get_config_parsed(key).await?.unwrap_or_default())
.await }
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
/// Returns boolean configuration value (if any) for the given key.
pub async fn get_config_bool_opt(&self, key: Config) -> Result<Option<bool>> {
Ok(self.get_config_parsed::<i32>(key).await?.map(|x| x != 0))
} }
/// Returns boolean configuration value for the given key. /// Returns boolean configuration value for the given key.
pub async fn get_config_bool(&self, key: Config) -> Result<bool> { pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
Ok(self.get_config_int(key).await? != 0) Ok(self.get_config_bool_opt(key).await?.unwrap_or_default())
} }
/// Returns true if movebox ("DeltaChat" folder) should be watched. /// Returns true if movebox ("DeltaChat" folder) should be watched.
@@ -550,7 +566,6 @@ fn get_config_keys_string() -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::str::FromStr;
use std::string::ToString; use std::string::ToString;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;

View File

@@ -201,7 +201,7 @@ pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640; pub const WORSE_IMAGE_SIZE: u32 = 640;
// this value can be increased if the folder configuration is changed and must be redone on next program start // this value can be increased if the folder configuration is changed and must be redone on next program start
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3; pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime}; use std::time::{Duration, Instant, SystemTime};
use anyhow::{bail, ensure, Result}; use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender}; use async_channel::{self as channel, Receiver, Sender};
use ratelimit::Ratelimit; use ratelimit::Ratelimit;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
@@ -623,6 +623,10 @@ impl Context {
.get_config(Config::ConfiguredMvboxFolder) .get_config(Config::ConfiguredMvboxFolder)
.await? .await?
.unwrap_or_else(|| "<unset>".to_string()); .unwrap_or_else(|| "<unset>".to_string());
let configured_trash_folder = self
.get_config(Config::ConfiguredTrashFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info(); let mut res = get_info();
@@ -689,6 +693,7 @@ impl Context {
res.insert("configured_inbox_folder", configured_inbox_folder); res.insert("configured_inbox_folder", configured_inbox_folder);
res.insert("configured_sentbox_folder", configured_sentbox_folder); res.insert("configured_sentbox_folder", configured_sentbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder); res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("configured_trash_folder", configured_trash_folder);
res.insert("mdns_enabled", mdns_enabled.to_string()); res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("e2ee_enabled", e2ee_enabled.to_string()); res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert( res.insert(
@@ -722,6 +727,12 @@ impl Context {
.await? .await?
.to_string(), .to_string(),
); );
res.insert(
"delete_to_trash",
self.get_config(Config::DeleteToTrash)
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert( res.insert(
"last_housekeeping", "last_housekeeping",
self.get_config_int(Config::LastHousekeeping) self.get_config_int(Config::LastHousekeeping)
@@ -887,6 +898,33 @@ impl Context {
Ok(mvbox.as_deref() == Some(folder_name)) Ok(mvbox.as_deref() == Some(folder_name))
} }
/// Returns true if given folder name is the name of the trash folder.
pub async fn is_trash(&self, folder_name: &str) -> Result<bool> {
let trash = self.get_config(Config::ConfiguredTrashFolder).await?;
Ok(trash.as_deref() == Some(folder_name))
}
pub(crate) async fn should_delete_to_trash(&self) -> Result<bool> {
if let Some(v) = self.get_config_bool_opt(Config::DeleteToTrash).await? {
return Ok(v);
}
if let Some(provider) = self.get_configured_provider().await? {
return Ok(provider.opt.delete_to_trash);
}
Ok(false)
}
/// Returns `target` for deleted messages as per `imap` table. Empty string means "delete w/o
/// moving to trash".
pub(crate) async fn get_delete_msgs_target(&self) -> Result<String> {
if !self.should_delete_to_trash().await? {
return Ok("".into());
}
self.get_config(Config::ConfiguredTrashFolder)
.await?
.context("No configured trash folder")
}
pub(crate) fn derive_blobdir(dbfile: &Path) -> PathBuf { pub(crate) fn derive_blobdir(dbfile: &Path) -> PathBuf {
let mut blob_fname = OsString::new(); let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default()); blob_fname.push(dbfile.file_name().unwrap_or_default());

View File

@@ -138,7 +138,7 @@ impl Job {
context context
.sql .sql
.query_row_optional( .query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''", "SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target=folder",
paramsv![msg.rfc724_mid], paramsv![msg.rfc724_mid],
|row| { |row| {
let server_uid: u32 = row.get(0)?; let server_uid: u32 = row.get(0)?;

View File

@@ -588,19 +588,25 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER), now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
), ),
}; };
let target = context.get_delete_msgs_target().await?;
context context
.sql .sql
.execute( .execute(
"UPDATE imap "UPDATE imap
SET target='' SET target=?
WHERE rfc724_mid IN ( WHERE rfc724_mid IN (
SELECT rfc724_mid FROM msgs SELECT rfc724_mid FROM msgs
WHERE ((download_state = 0 AND timestamp < ?) OR WHERE ((download_state = 0 AND timestamp < ?) OR
(download_state != 0 AND timestamp < ?) OR (download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?)) (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
)", )",
paramsv![threshold_timestamp, threshold_timestamp_extended, now], paramsv![
target,
threshold_timestamp,
threshold_timestamp_extended,
now,
],
) )
.await?; .await?;

View File

@@ -113,13 +113,15 @@ impl async_imap::Authenticator for OAuth2 {
} }
} }
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
enum FolderMeaning { pub enum FolderMeaning {
Unknown, Unknown,
Spam, Spam,
Inbox,
Mvbox,
Sent, Sent,
Trash,
Drafts, Drafts,
Other,
/// Virtual folders. /// Virtual folders.
/// ///
@@ -131,13 +133,15 @@ enum FolderMeaning {
} }
impl FolderMeaning { impl FolderMeaning {
fn to_config(self) -> Option<Config> { pub fn to_config(self) -> Option<Config> {
match self { match self {
FolderMeaning::Unknown => None, FolderMeaning::Unknown => None,
FolderMeaning::Spam => None, FolderMeaning::Spam => None,
FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder), FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
FolderMeaning::Drafts => None, FolderMeaning::Drafts => None,
FolderMeaning::Other => None,
FolderMeaning::Virtual => None, FolderMeaning::Virtual => None,
} }
} }
@@ -449,7 +453,7 @@ impl Imap {
&mut self, &mut self,
context: &Context, context: &Context,
watch_folder: &str, watch_folder: &str,
is_spam_folder: bool, folder_meaning: FolderMeaning,
) -> Result<()> { ) -> Result<()> {
if !context.sql.is_open().await { if !context.sql.is_open().await {
// probably shutdown // probably shutdown
@@ -458,7 +462,7 @@ impl Imap {
self.prepare(context).await?; self.prepare(context).await?;
let msgs_fetched = self let msgs_fetched = self
.fetch_new_messages(context, watch_folder, is_spam_folder, false) .fetch_new_messages(context, 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() {
@@ -490,49 +494,60 @@ impl Imap {
pub(crate) async fn resync_folder_uids( pub(crate) async fn resync_folder_uids(
&mut self, &mut self,
context: &Context, context: &Context,
folder: String, folder: &str,
folder_meaning: FolderMeaning,
) -> Result<()> { ) -> Result<()> {
// Collect pairs of UID and Message-ID. // Collect pairs of UID and Message-ID.
let mut msg_ids = BTreeMap::new(); let mut msgs = BTreeMap::new();
let session = self let session = self
.session .session
.as_mut() .as_mut()
.context("IMAP No connection established")?; .context("IMAP No connection established")?;
session.select_folder(context, Some(&folder)).await?; session.select_folder(context, Some(folder)).await?;
let mut list = session let mut list = session
.uid_fetch("1:*", RFC724MID_UID) .uid_fetch("1:*", RFC724MID_UID)
.await .await
.with_context(|| format!("can't resync folder {folder}"))?; .with_context(|| format!("can't resync folder {folder}"))?;
while let Some(fetch) = list.next().await { while let Some(fetch) = list.next().await {
let msg = fetch?; let fetch = fetch?;
let headers = match get_fetch_headers(&fetch) {
Ok(headers) => headers,
Err(err) => {
warn!(context, "Failed to parse FETCH headers: {}", err);
continue;
}
};
let message_id = prefetch_get_message_id(&headers);
// Get Message-ID if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
let message_id = msgs.insert(
get_fetch_headers(&msg).map_or(None, |headers| prefetch_get_message_id(&headers)); uid,
(
if let (Some(uid), Some(rfc724_mid)) = (msg.uid, message_id) { rfc724_mid,
msg_ids.insert(uid, rfc724_mid); target_folder(context, folder, folder_meaning, &headers).await?,
),
);
} }
} }
info!( info!(
context, context,
"Resync: collected {} message IDs in folder {}", "Resync: collected {} message IDs in folder {}",
msg_ids.len(), msgs.len(),
&folder folder,
); );
let uid_validity = get_uidvalidity(context, &folder).await?; let uid_validity = get_uidvalidity(context, folder).await?;
// Write collected UIDs to SQLite database. // Write collected UIDs to SQLite database.
context context
.sql .sql
.transaction(move |transaction| { .transaction(move |transaction| {
transaction.execute("DELETE FROM imap WHERE folder=?", params![folder])?; transaction.execute("DELETE FROM imap WHERE folder=?", params![folder])?;
for (uid, rfc724_mid) in &msg_ids { for (uid, (rfc724_mid, target)) in &msgs {
// This may detect previously undetected moved // This may detect previously undetected moved
// messages, so we update server_folder too. // messages, so we update server_folder too.
transaction.execute( transaction.execute(
@@ -541,7 +556,7 @@ impl Imap {
ON CONFLICT(folder, uid, uidvalidity) ON CONFLICT(folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid, DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target", target=excluded.target",
params![rfc724_mid, folder, uid, uid_validity, folder], params![rfc724_mid, folder, uid, uid_validity, target],
)?; )?;
} }
Ok(()) Ok(())
@@ -683,10 +698,10 @@ impl Imap {
&mut self, &mut self,
context: &Context, context: &Context,
folder: &str, folder: &str,
is_spam_folder: bool, folder_meaning: FolderMeaning,
fetch_existing_msgs: bool, fetch_existing_msgs: bool,
) -> Result<bool> { ) -> Result<bool> {
if should_ignore_folder(context, folder, is_spam_folder).await? { if should_ignore_folder(context, folder, folder_meaning).await? {
info!(context, "Not fetching from {}", folder); info!(context, "Not fetching from {}", folder);
return Ok(false); return Ok(false);
} }
@@ -732,14 +747,7 @@ impl Imap {
// Get the Message-ID or generate a fake one to identify the message in the database. // Get the Message-ID or generate a fake one to identify the message in the database.
let message_id = prefetch_get_or_create_message_id(&headers); let message_id = prefetch_get_or_create_message_id(&headers);
let target = target_folder(context, folder, folder_meaning, &headers).await?;
let target = match target_folder(context, folder, is_spam_folder, &headers).await? {
Some(config) => match context.get_config(config).await? {
Some(target) => target,
None => folder.to_string(),
},
None => folder.to_string(),
};
context context
.sql .sql
@@ -763,8 +771,8 @@ impl Imap {
// Never download messages directly from the spam folder. // Never download messages directly from the spam folder.
// If the sender is known, the message will be moved to the Inbox or Mvbox // If the sender is known, the message will be moved to the Inbox or Mvbox
// and then we download the message from there. // and then we download the message from there.
// Also see `spam_target_folder()`. // Also see `spam_target_folder_cfg()`.
&& !is_spam_folder && folder_meaning != FolderMeaning::Spam
&& prefetch_should_download( && prefetch_should_download(
context, context,
&headers, &headers,
@@ -870,17 +878,21 @@ impl Imap {
.context("failed to get recipients from the inbox")?; .context("failed to get recipients from the inbox")?;
if context.get_config_bool(Config::FetchExistingMsgs).await? { if context.get_config_bool(Config::FetchExistingMsgs).await? {
for config in &[ for meaning in [
Config::ConfiguredMvboxFolder, FolderMeaning::Mvbox,
Config::ConfiguredInboxFolder, FolderMeaning::Inbox,
Config::ConfiguredSentboxFolder, FolderMeaning::Sent,
] { ] {
if let Some(folder) = context.get_config(*config).await? { let config = match meaning.to_config() {
Some(c) => c,
None => continue,
};
if let Some(folder) = context.get_config(config).await? {
info!( info!(
context, context,
"Fetching existing messages from folder \"{}\"", folder "Fetching existing messages from folder \"{}\"", folder
); );
self.fetch_new_messages(context, &folder, false, true) self.fetch_new_messages(context, &folder, meaning, true)
.await .await
.context("could not fetch existing messages")?; .context("could not fetch existing messages")?;
} }
@@ -952,44 +964,60 @@ impl Session {
return Ok(()); return Ok(());
} }
Err(err) => { Err(err) => {
if context.should_delete_to_trash().await? {
error!(
context,
"Cannot move messages {} to {}, no fallback to COPY/DELETE because \
delete_to_trash is set. Error: {:#}",
set,
target,
err,
);
return Err(err.into());
}
warn!( warn!(
context, context,
"Cannot move message, fallback to COPY/DELETE {} to {}: {}", "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
set, set,
target, target,
err err
); );
} }
} }
} else { }
// Server does not support MOVE or MOVE failed.
// Copy messages to the destination folder if needed and mark records for deletion.
let copy = !context.is_trash(target).await?;
if copy {
info!( info!(
context, context,
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
); );
self.uid_copy(&set, &target).await?;
} else {
error!(
context,
"Server does not support MOVE, fallback to DELETE {} to {}", set, target,
);
} }
context
// Server does not support MOVE or MOVE failed. .sql
// Copy the message to the destination folder and mark the record for deletion. .execute(
match self.uid_copy(&set, &target).await { &format!(
Ok(()) => { "UPDATE imap SET target='' WHERE id IN ({})",
context sql::repeat_vars(row_ids.len())
.sql ),
.execute( rusqlite::params_from_iter(row_ids),
&format!( )
"UPDATE imap SET target='' WHERE id IN ({})", .await
sql::repeat_vars(row_ids.len()) .context("cannot plan deletion of messages")?;
), if copy {
rusqlite::params_from_iter(row_ids), context.emit_event(EventType::ImapMessageMoved(format!(
) "IMAP messages {set} copied to {target}"
.await )));
.context("cannot plan deletion of copied messages")?;
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP messages {set} copied to {target}"
)));
Ok(())
}
Err(err) => Err(err.into()),
} }
Ok(())
} }
/// Moves and deletes messages as planned in the `imap` table. /// Moves and deletes messages as planned in the `imap` table.
@@ -1644,7 +1672,7 @@ impl Imap {
} }
} }
let folder_meaning = get_folder_meaning(&folder); let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
let folder_name_meaning = get_folder_meaning_by_name(folder.name()); let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if let Some(config) = folder_meaning.to_config() { if let Some(config) = folder_meaning.to_config() {
// Always takes precedence // Always takes precedence
@@ -1776,7 +1804,7 @@ async fn should_move_out_of_spam(
/// If this returns None, the message will not be moved out of the /// If this returns None, the message will not be moved out of the
/// Spam folder, and as `fetch_new_messages()` doesn't download /// Spam folder, and as `fetch_new_messages()` doesn't download
/// messages from the Spam folder, the message will be ignored. /// messages from the Spam folder, the message will be ignored.
async fn spam_target_folder( async fn spam_target_folder_cfg(
context: &Context, context: &Context,
headers: &[mailparse::MailHeader<'_>], headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Config>> { ) -> Result<Option<Config>> {
@@ -1797,18 +1825,18 @@ async fn spam_target_folder(
/// Returns `ConfiguredInboxFolder`, `ConfiguredMvboxFolder` or `ConfiguredSentboxFolder` if /// Returns `ConfiguredInboxFolder`, `ConfiguredMvboxFolder` or `ConfiguredSentboxFolder` if
/// the message needs to be moved from `folder`. Otherwise returns `None`. /// the message needs to be moved from `folder`. Otherwise returns `None`.
pub async fn target_folder( pub async fn target_folder_cfg(
context: &Context, context: &Context,
folder: &str, folder: &str,
is_spam_folder: bool, folder_meaning: FolderMeaning,
headers: &[mailparse::MailHeader<'_>], headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Config>> { ) -> Result<Option<Config>> {
if context.is_mvbox(folder).await? { if context.is_mvbox(folder).await? {
return Ok(None); return Ok(None);
} }
if is_spam_folder { if folder_meaning == FolderMeaning::Spam {
spam_target_folder(context, headers).await spam_target_folder_cfg(context, headers).await
} else if needs_move_to_mvbox(context, headers).await? { } else if needs_move_to_mvbox(context, headers).await? {
Ok(Some(Config::ConfiguredMvboxFolder)) Ok(Some(Config::ConfiguredMvboxFolder))
} else { } else {
@@ -1816,6 +1844,21 @@ pub async fn target_folder(
} }
} }
pub async fn target_folder(
context: &Context,
folder: &str,
folder_meaning: FolderMeaning,
headers: &[mailparse::MailHeader<'_>],
) -> Result<String> {
match target_folder_cfg(context, folder, folder_meaning, headers).await? {
Some(config) => match context.get_config(config).await? {
Some(target) => Ok(target),
None => Ok(folder.to_string()),
},
None => Ok(folder.to_string()),
}
}
async fn needs_move_to_mvbox( async fn needs_move_to_mvbox(
context: &Context, context: &Context,
headers: &[mailparse::MailHeader<'_>], headers: &[mailparse::MailHeader<'_>],
@@ -1940,10 +1983,10 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
} }
} }
fn get_folder_meaning(folder_name: &Name) -> FolderMeaning { fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
for attr in folder_name.attributes() { for attr in folder_attrs {
match attr { match attr {
NameAttribute::Trash => return FolderMeaning::Other, NameAttribute::Trash => return FolderMeaning::Trash,
NameAttribute::Sent => return FolderMeaning::Sent, NameAttribute::Sent => return FolderMeaning::Sent,
NameAttribute::Junk => return FolderMeaning::Spam, NameAttribute::Junk => return FolderMeaning::Spam,
NameAttribute::Drafts => return FolderMeaning::Drafts, NameAttribute::Drafts => return FolderMeaning::Drafts,
@@ -1961,6 +2004,13 @@ fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
FolderMeaning::Unknown FolderMeaning::Unknown
} }
pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
match get_folder_meaning_by_attrs(folder.attributes()) {
FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
meaning => meaning,
}
}
/// Parses the headers from the FETCH result. /// Parses the headers from the FETCH result.
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> { fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
match prefetch_msg.header() { match prefetch_msg.header() {
@@ -2272,7 +2322,7 @@ pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result
async fn should_ignore_folder( async fn should_ignore_folder(
context: &Context, context: &Context,
folder: &str, folder: &str,
is_spam_folder: bool, folder_meaning: FolderMeaning,
) -> Result<bool> { ) -> Result<bool> {
if !context.get_config_bool(Config::OnlyFetchMvbox).await? { if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
return Ok(false); return Ok(false);
@@ -2281,7 +2331,7 @@ async fn should_ignore_folder(
// Still respect the SentboxWatch setting. // Still respect the SentboxWatch setting.
return Ok(!context.get_config_bool(Config::SentboxWatch).await?); return Ok(!context.get_config_bool(Config::SentboxWatch).await?);
} }
Ok(!(context.is_mvbox(folder).await? || is_spam_folder)) Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
} }
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000 /// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
@@ -2564,14 +2614,13 @@ mod tests {
}; };
let (headers, _) = mailparse::parse_headers(bytes)?; let (headers, _) = mailparse::parse_headers(bytes)?;
let actual = if let Some(config) =
let is_spam_folder = folder == "Spam"; target_folder_cfg(&t, folder, get_folder_meaning_by_name(folder), &headers).await?
let actual = {
if let Some(config) = target_folder(&t, folder, is_spam_folder, &headers).await? { t.get_config(config).await?
t.get_config(config).await? } else {
} else { None
None };
};
let expected = if expected_destination == folder { let expected = if expected_destination == folder {
None None

View File

@@ -7,7 +7,7 @@ use futures_lite::FutureExt;
use super::session::Session; use super::session::Session;
use super::Imap; use super::Imap;
use crate::imap::client::IMAP_TIMEOUT; use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
use crate::{context::Context, scheduler::InterruptInfo}; use crate::{context::Context, scheduler::InterruptInfo};
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60); const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
@@ -113,6 +113,7 @@ impl Imap {
&mut self, &mut self,
context: &Context, context: &Context,
watch_folder: Option<String>, watch_folder: Option<String>,
folder_meaning: FolderMeaning,
) -> InterruptInfo { ) -> InterruptInfo {
// Idle using polling. This is also needed if we're not yet configured - // 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). // in this case, we're waiting for a configure job (and an interrupt).
@@ -173,7 +174,7 @@ impl Imap {
// 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 match self
.fetch_new_messages(context, &watch_folder, false, false) .fetch_new_messages(context, &watch_folder, folder_meaning, false)
.await .await
{ {
Ok(res) => { Ok(res) => {

View File

@@ -3,7 +3,7 @@ use std::{collections::BTreeMap, time::Instant};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use futures::stream::StreamExt; use futures::stream::StreamExt;
use super::{get_folder_meaning, 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::Imap;
use crate::log::LogExt; use crate::log::LogExt;
@@ -33,7 +33,7 @@ impl Imap {
let mut folder_configs = BTreeMap::new(); let mut folder_configs = BTreeMap::new();
for folder in folders { for folder in folders {
let folder_meaning = get_folder_meaning(&folder); let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
if folder_meaning == FolderMeaning::Virtual { if folder_meaning == FolderMeaning::Virtual {
// Gmail has virtual folders that should be skipped. For example, // Gmail has virtual folders that should be skipped. For example,
// emails appear in the inbox and under "All Mail" as soon as it is // emails appear in the inbox and under "All Mail" as soon as it is
@@ -53,21 +53,22 @@ impl Imap {
.or_insert_with(|| folder.name().to_string()); .or_insert_with(|| folder.name().to_string());
} }
let is_drafts = folder_meaning == FolderMeaning::Drafts let folder_meaning = match folder_meaning {
|| (folder_meaning == FolderMeaning::Unknown FolderMeaning::Unknown => folder_name_meaning,
&& folder_name_meaning == FolderMeaning::Drafts); _ => folder_meaning,
let is_spam_folder = folder_meaning == FolderMeaning::Spam };
|| (folder_meaning == FolderMeaning::Unknown
&& folder_name_meaning == FolderMeaning::Spam);
// Don't scan folders that are watched anyway // Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts { if !watched_folders.contains(&folder.name().to_string())
&& folder_meaning != FolderMeaning::Drafts
&& folder_meaning != FolderMeaning::Trash
{
let session = self.session.as_mut().context("no session")?; 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(), is_spam_folder) self.fetch_move_delete(context, folder.name(), folder_meaning)
.await .await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder"); .ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
@@ -80,15 +81,15 @@ impl Imap {
} }
} }
// Set the `ConfiguredSentboxFolder` or set it to `None` if the folder was deleted. // Set configs for necessary folders. Or reset if the folder was deleted.
context for conf in [
.set_config( Config::ConfiguredSentboxFolder,
Config::ConfiguredSentboxFolder, Config::ConfiguredTrashFolder,
folder_configs ] {
.get(&Config::ConfiguredSentboxFolder) context
.map(|s| s.as_str()), .set_config(conf, folder_configs.get(&conf).map(|s| s.as_str()))
) .await?;
.await?; }
last_scan.replace(Instant::now()); last_scan.replace(Instant::now());
Ok(true) Ok(true)

View File

@@ -12,7 +12,7 @@ use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use crate::context::Context; use crate::context::Context;
use crate::imap::Imap; use crate::imap::{get_folder_meaning, FolderMeaning, Imap};
use crate::param::Params; use crate::param::Params;
use crate::scheduler::InterruptInfo; use crate::scheduler::InterruptInfo;
use crate::tools::time; use crate::tools::time;
@@ -172,8 +172,12 @@ impl Job {
let mut any_failed = false; let mut any_failed = false;
for folder in all_folders { for folder in all_folders {
let folder_meaning = get_folder_meaning(&folder);
if folder_meaning == FolderMeaning::Virtual {
continue;
}
if let Err(e) = imap if let Err(e) = imap
.resync_folder_uids(context, folder.name().to_string()) .resync_folder_uids(context, folder.name(), folder_meaning)
.await .await
{ {
warn!(context, "{:#}", e); warn!(context, "{:#}", e);

View File

@@ -1386,11 +1386,12 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: *msg_id }); context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: *msg_id });
} }
let target = context.get_delete_msgs_target().await?;
context context
.sql .sql
.execute( .execute(
"UPDATE imap SET target='' WHERE rfc724_mid=?", "UPDATE imap SET target=? WHERE rfc724_mid=?",
paramsv![msg.rfc724_mid], paramsv![target, msg.rfc724_mid],
) )
.await?; .await?;

View File

@@ -138,6 +138,16 @@ pub struct Provider {
/// Type of OAuth 2 authorization if provider supports it. /// Type of OAuth 2 authorization if provider supports it.
pub oauth2_authorizer: Option<Oauth2Authorizer>, pub oauth2_authorizer: Option<Oauth2Authorizer>,
/// Options with good defaults.
pub opt: ProviderOptions,
}
/// Provider options with good defaults.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct ProviderOptions {
/// Move messages to the Trash folder instead of marking them "\Deleted".
pub delete_to_trash: bool,
} }
/// Get resolver to query MX records. /// Get resolver to query MX records.

View File

@@ -7,7 +7,9 @@ use once_cell::sync::Lazy;
use crate::provider::Protocol::*; use crate::provider::Protocol::*;
use crate::provider::Socket::*; use crate::provider::Socket::*;
use crate::provider::UsernamePattern::*; use crate::provider::UsernamePattern::*;
use crate::provider::{Config, ConfigDefault, Oauth2Authorizer, Provider, Server, Status}; use crate::provider::{
Config, ConfigDefault, Oauth2Authorizer, Provider, ProviderOptions, Server, Status,
};
// 163.md: 163.com // 163.md: 163.com
static P_163: Lazy<Provider> = Lazy::new(|| Provider { static P_163: Lazy<Provider> = Lazy::new(|| Provider {
@@ -36,6 +38,7 @@ static P_163: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// aktivix.org.md: aktivix.org // aktivix.org.md: aktivix.org
@@ -65,6 +68,7 @@ static P_AKTIVIX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// aol.md: aol.com // aol.md: aol.com
@@ -83,6 +87,7 @@ static P_AOL: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -113,6 +118,7 @@ static P_ARCOR_DE: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// autistici.org.md: autistici.org // autistici.org.md: autistici.org
@@ -142,6 +148,7 @@ static P_AUTISTICI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// blindzeln.org.md: delta.blinzeln.de, delta.blindzeln.org // blindzeln.org.md: delta.blinzeln.de, delta.blindzeln.org
@@ -171,6 +178,7 @@ static P_BLINDZELN_ORG: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// bluewin.ch.md: bluewin.ch // bluewin.ch.md: bluewin.ch
@@ -200,6 +208,7 @@ static P_BLUEWIN_CH: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// buzon.uy.md: buzon.uy // buzon.uy.md: buzon.uy
@@ -229,6 +238,7 @@ static P_BUZON_UY: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// chello.at.md: chello.at // chello.at.md: chello.at
@@ -258,6 +268,7 @@ static P_CHELLO_AT: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// comcast.md: xfinity.com, comcast.net // comcast.md: xfinity.com, comcast.net
@@ -272,6 +283,7 @@ static P_COMCAST: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// dismail.de.md: dismail.de // dismail.de.md: dismail.de
@@ -286,6 +298,7 @@ static P_DISMAIL_DE: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// disroot.md: disroot.org // disroot.md: disroot.org
@@ -315,6 +328,7 @@ static P_DISROOT: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// e.email.md: e.email // e.email.md: e.email
@@ -344,6 +358,7 @@ static P_E_EMAIL: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// espiv.net.md: espiv.net // espiv.net.md: espiv.net
@@ -358,6 +373,7 @@ static P_ESPIV_NET: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// example.com.md: example.com, example.org, example.net // example.com.md: example.com, example.org, example.net
@@ -376,6 +392,7 @@ static P_EXAMPLE_COM: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -407,6 +424,7 @@ static P_FASTMAIL: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// firemail.de.md: firemail.at, firemail.de // firemail.de.md: firemail.at, firemail.de
@@ -423,6 +441,7 @@ static P_FIREMAIL_DE: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -451,6 +470,7 @@ static P_FIVE_CHAT: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// freenet.de.md: freenet.de // freenet.de.md: freenet.de
@@ -469,6 +489,7 @@ static P_FREENET_DE: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -488,6 +509,9 @@ static P_GMAIL: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: Some(Oauth2Authorizer::Gmail), oauth2_authorizer: Some(Oauth2Authorizer::Gmail),
opt: ProviderOptions {
delete_to_trash: true,
},
} }
}); });
@@ -525,6 +549,7 @@ static P_GMX_NET: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// hermes.radio.md: ac.hermes.radio, ac1.hermes.radio, ac2.hermes.radio, ac3.hermes.radio, ac4.hermes.radio, ac5.hermes.radio, ac6.hermes.radio, ac7.hermes.radio, ac8.hermes.radio, ac9.hermes.radio, ac10.hermes.radio, ac11.hermes.radio, ac12.hermes.radio, ac13.hermes.radio, ac14.hermes.radio, ac15.hermes.radio, ka.hermes.radio, ka1.hermes.radio, ka2.hermes.radio, ka3.hermes.radio, ka4.hermes.radio, ka5.hermes.radio, ka6.hermes.radio, ka7.hermes.radio, ka8.hermes.radio, ka9.hermes.radio, ka10.hermes.radio, ka11.hermes.radio, ka12.hermes.radio, ka13.hermes.radio, ka14.hermes.radio, ka15.hermes.radio, ec.hermes.radio, ec1.hermes.radio, ec2.hermes.radio, ec3.hermes.radio, ec4.hermes.radio, ec5.hermes.radio, ec6.hermes.radio, ec7.hermes.radio, ec8.hermes.radio, ec9.hermes.radio, ec10.hermes.radio, ec11.hermes.radio, ec12.hermes.radio, ec13.hermes.radio, ec14.hermes.radio, ec15.hermes.radio, hermes.radio // hermes.radio.md: ac.hermes.radio, ac1.hermes.radio, ac2.hermes.radio, ac3.hermes.radio, ac4.hermes.radio, ac5.hermes.radio, ac6.hermes.radio, ac7.hermes.radio, ac8.hermes.radio, ac9.hermes.radio, ac10.hermes.radio, ac11.hermes.radio, ac12.hermes.radio, ac13.hermes.radio, ac14.hermes.radio, ac15.hermes.radio, ka.hermes.radio, ka1.hermes.radio, ka2.hermes.radio, ka3.hermes.radio, ka4.hermes.radio, ka5.hermes.radio, ka6.hermes.radio, ka7.hermes.radio, ka8.hermes.radio, ka9.hermes.radio, ka10.hermes.radio, ka11.hermes.radio, ka12.hermes.radio, ka13.hermes.radio, ka14.hermes.radio, ka15.hermes.radio, ec.hermes.radio, ec1.hermes.radio, ec2.hermes.radio, ec3.hermes.radio, ec4.hermes.radio, ec5.hermes.radio, ec6.hermes.radio, ec7.hermes.radio, ec8.hermes.radio, ec9.hermes.radio, ec10.hermes.radio, ec11.hermes.radio, ec12.hermes.radio, ec13.hermes.radio, ec14.hermes.radio, ec15.hermes.radio, hermes.radio
@@ -552,6 +577,7 @@ static P_HERMES_RADIO: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: false, strict_tls: false,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// hey.com.md: hey.com // hey.com.md: hey.com
@@ -568,6 +594,7 @@ static P_HEY_COM: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -583,6 +610,7 @@ static P_I_UA: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// i3.net.md: i3.net // i3.net.md: i3.net
@@ -597,6 +625,7 @@ static P_I3_NET: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// icloud.md: icloud.com, me.com, mac.com // icloud.md: icloud.com, me.com, mac.com
@@ -626,6 +655,7 @@ static P_ICLOUD: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// infomaniak.com.md: ik.me // infomaniak.com.md: ik.me
@@ -655,6 +685,7 @@ static P_INFOMANIAK_COM: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: Some(10), max_smtp_rcpt_to: Some(10),
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// kolst.com.md: kolst.com // kolst.com.md: kolst.com
@@ -669,6 +700,7 @@ static P_KOLST_COM: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// kontent.com.md: kontent.com // kontent.com.md: kontent.com
@@ -683,6 +715,7 @@ static P_KONTENT_COM: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// mail.de.md: mail.de // mail.de.md: mail.de
@@ -712,6 +745,7 @@ static P_MAIL_DE: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// mail.ru.md: mail.ru, inbox.ru, internet.ru, bk.ru, list.ru // mail.ru.md: mail.ru, inbox.ru, internet.ru, bk.ru, list.ru
@@ -730,6 +764,7 @@ static P_MAIL_RU: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -760,6 +795,7 @@ static P_MAIL2TOR: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// mailbox.org.md: mailbox.org, secure.mailbox.org // mailbox.org.md: mailbox.org, secure.mailbox.org
@@ -789,6 +825,7 @@ static P_MAILBOX_ORG: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// mailo.com.md: mailo.com // mailo.com.md: mailo.com
@@ -818,6 +855,7 @@ static P_MAILO_COM: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// nauta.cu.md: nauta.cu // nauta.cu.md: nauta.cu
@@ -872,6 +910,7 @@ static P_NAUTA_CU: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: false, strict_tls: false,
max_smtp_rcpt_to: Some(20), max_smtp_rcpt_to: Some(20),
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// naver.md: naver.com // naver.md: naver.com
@@ -901,6 +940,7 @@ static P_NAVER: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// nubo.coop.md: nubo.coop // nubo.coop.md: nubo.coop
@@ -930,6 +970,7 @@ static P_NUBO_COOP: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de // outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de
@@ -959,6 +1000,7 @@ static P_OUTLOOK_COM: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// ouvaton.coop.md: ouvaton.org // ouvaton.coop.md: ouvaton.org
@@ -988,6 +1030,7 @@ static P_OUVATON_COOP: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us // posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
@@ -1017,6 +1060,7 @@ static P_POSTEO: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// protonmail.md: protonmail.com, protonmail.ch, pm.me // protonmail.md: protonmail.com, protonmail.ch, pm.me
@@ -1033,6 +1077,7 @@ static P_PROTONMAIL: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -1052,6 +1097,7 @@ static P_QQ: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -1082,6 +1128,7 @@ static P_RISEUP_NET: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// rogers.com.md: rogers.com // rogers.com.md: rogers.com
@@ -1096,6 +1143,7 @@ static P_ROGERS_COM: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// systemausfall.org.md: systemausfall.org, solidaris.me // systemausfall.org.md: systemausfall.org, solidaris.me
@@ -1125,6 +1173,7 @@ static P_SYSTEMAUSFALL_ORG: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// systemli.org.md: systemli.org // systemli.org.md: systemli.org
@@ -1154,6 +1203,7 @@ static P_SYSTEMLI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// t-online.md: t-online.de, magenta.de // t-online.md: t-online.de, magenta.de
@@ -1172,6 +1222,7 @@ static P_T_ONLINE: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -1222,6 +1273,7 @@ static P_TESTRUN: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// tiscali.it.md: tiscali.it // tiscali.it.md: tiscali.it
@@ -1251,6 +1303,7 @@ static P_TISCALI_IT: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// tutanota.md: tutanota.com, tutanota.de, tutamail.com, tuta.io, keemail.me // tutanota.md: tutanota.com, tutanota.de, tutamail.com, tuta.io, keemail.me
@@ -1267,6 +1320,7 @@ static P_TUTANOTA: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -1282,6 +1336,7 @@ static P_UKR_NET: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// undernet.uy.md: undernet.uy // undernet.uy.md: undernet.uy
@@ -1311,6 +1366,7 @@ static P_UNDERNET_UY: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// vfemail.md: vfemail.net // vfemail.md: vfemail.net
@@ -1325,6 +1381,7 @@ static P_VFEMAIL: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// vivaldi.md: vivaldi.net // vivaldi.md: vivaldi.net
@@ -1354,6 +1411,7 @@ static P_VIVALDI: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// vodafone.de.md: vodafone.de, vodafonemail.de // vodafone.de.md: vodafone.de, vodafonemail.de
@@ -1383,6 +1441,7 @@ static P_VODAFONE_DE: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// web.de.md: web.de, email.de, flirt.ms, hallo.ms, kuss.ms, love.ms, magic.ms, singles.ms, cool.ms, kanzler.ms, okay.ms, party.ms, pop.ms, stars.ms, techno.ms, clever.ms, deutschland.ms, genial.ms, ich.ms, online.ms, smart.ms, wichtig.ms, action.ms, fussball.ms, joker.ms, planet.ms, power.ms // web.de.md: web.de, email.de, flirt.ms, hallo.ms, kuss.ms, love.ms, magic.ms, singles.ms, cool.ms, kanzler.ms, okay.ms, party.ms, pop.ms, stars.ms, techno.ms, clever.ms, deutschland.ms, genial.ms, ich.ms, online.ms, smart.ms, wichtig.ms, action.ms, fussball.ms, joker.ms, planet.ms, power.ms
@@ -1402,6 +1461,7 @@ static P_WEB_DE: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -1421,6 +1481,7 @@ static P_YAHOO: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -1451,6 +1512,7 @@ static P_YANDEX_RU: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: Some(Oauth2Authorizer::Yandex), oauth2_authorizer: Some(Oauth2Authorizer::Yandex),
opt: Default::default(),
}); });
// yggmail.md: yggmail // yggmail.md: yggmail
@@ -1471,6 +1533,7 @@ static P_YGGMAIL: Lazy<Provider> = Lazy::new(|| {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
} }
}); });
@@ -1501,6 +1564,7 @@ static P_ZIGGO_NL: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
// zoho.md: zohomail.eu, zohomail.com, zoho.com // zoho.md: zohomail.eu, zohomail.com, zoho.com
@@ -1530,6 +1594,7 @@ static P_ZOHO: Lazy<Provider> = Lazy::new(|| Provider {
strict_tls: true, strict_tls: true,
max_smtp_rcpt_to: None, max_smtp_rcpt_to: None,
oauth2_authorizer: None, oauth2_authorizer: None,
opt: Default::default(),
}); });
pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| { pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| {

View File

@@ -342,11 +342,12 @@ pub(crate) async fn receive_imf_inner(
if received_msg.needs_delete_job if received_msg.needs_delete_job
|| (delete_server_after == Some(0) && is_partial_download.is_none()) || (delete_server_after == Some(0) && is_partial_download.is_none())
{ {
let target = context.get_delete_msgs_target().await?;
context context
.sql .sql
.execute( .execute(
"UPDATE imap SET target='' WHERE rfc724_mid=?", "UPDATE imap SET target=? WHERE rfc724_mid=?",
paramsv![rfc724_mid], paramsv![target, rfc724_mid],
) )
.await?; .await?;
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() { } else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {

View File

@@ -1,6 +1,8 @@
use std::iter::{self, once};
use anyhow::{bail, Context as _, Result}; use anyhow::{bail, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender}; use async_channel::{self as channel, Receiver, Sender};
use futures::try_join; use futures::future::try_join_all;
use futures_lite::FutureExt; use futures_lite::FutureExt;
use tokio::task; use tokio::task;
@@ -9,7 +11,7 @@ use crate::config::Config;
use crate::contact::{ContactId, RecentlySeenLoop}; use crate::contact::{ContactId, RecentlySeenLoop};
use crate::context::Context; use crate::context::Context;
use crate::ephemeral::{self, delete_expired_imap_messages}; use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::imap::Imap; use crate::imap::{FolderMeaning, Imap};
use crate::job; use crate::job;
use crate::location; use crate::location;
use crate::log::LogExt; use crate::log::LogExt;
@@ -20,15 +22,19 @@ use crate::tools::{duration_to_str, maybe_add_time_based_warnings};
pub(crate) mod connectivity; pub(crate) mod connectivity;
#[derive(Debug)]
struct SchedBox {
meaning: FolderMeaning,
conn_state: ImapConnectionState,
handle: task::JoinHandle<()>,
}
/// Job and connection scheduler. /// Job and connection scheduler.
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Scheduler { pub(crate) struct Scheduler {
inbox: ImapConnectionState, inbox: SchedBox,
inbox_handle: task::JoinHandle<()>, /// Optional boxes -- mvbox, sentbox.
mvbox: ImapConnectionState, oboxes: Vec<SchedBox>,
mvbox_handle: Option<task::JoinHandle<()>>,
sentbox: ImapConnectionState,
sentbox_handle: Option<task::JoinHandle<()>>,
smtp: SmtpConnectionState, smtp: SmtpConnectionState,
smtp_handle: task::JoinHandle<()>, smtp_handle: task::JoinHandle<()>,
ephemeral_handle: task::JoinHandle<()>, ephemeral_handle: task::JoinHandle<()>,
@@ -161,7 +167,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
} }
} }
info = fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await; info = fetch_idle(&ctx, &mut connection, FolderMeaning::Inbox).await;
} }
} }
} }
@@ -182,7 +188,20 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
/// handling all the errors. In case of an error, it is logged, but not propagated upwards. If /// handling all the errors. In case of an error, it is logged, but not propagated upwards. If
/// critical operation fails such as fetching new messages fails, connection is reset via /// critical operation fails such as fetching new messages fails, connection is reset via
/// `trigger_reconnect`, so a fresh one can be opened. /// `trigger_reconnect`, so a fresh one can be opened.
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config) -> InterruptInfo { async fn fetch_idle(
ctx: &Context,
connection: &mut Imap,
folder_meaning: FolderMeaning,
) -> InterruptInfo {
let folder_config = match folder_meaning.to_config() {
Some(c) => c,
None => {
error!(ctx, "Bad folder meaning: {}", folder_meaning);
return connection
.fake_idle(ctx, None, FolderMeaning::Unknown)
.await;
}
};
let folder = match ctx.get_config(folder_config).await { let folder = match ctx.get_config(folder_config).await {
Ok(folder) => folder, Ok(folder) => folder,
Err(err) => { Err(err) => {
@@ -190,7 +209,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
ctx, ctx,
"Can not watch {} folder, failed to retrieve config: {:#}", folder_config, err "Can not watch {} folder, failed to retrieve config: {:#}", folder_config, err
); );
return connection.fake_idle(ctx, None).await; return connection
.fake_idle(ctx, None, FolderMeaning::Unknown)
.await;
} }
}; };
@@ -199,7 +220,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
} else { } else {
connection.connectivity.set_not_configured(ctx).await; connection.connectivity.set_not_configured(ctx).await;
info!(ctx, "Can not watch {} folder, not set", folder_config); info!(ctx, "Can not watch {} folder, not set", folder_config);
return connection.fake_idle(ctx, None).await; return connection
.fake_idle(ctx, None, FolderMeaning::Unknown)
.await;
}; };
// connect and fake idle if unable to connect // connect and fake idle if unable to connect
@@ -210,7 +233,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
{ {
warn!(ctx, "{:#}", err); warn!(ctx, "{:#}", err);
connection.trigger_reconnect(ctx); connection.trigger_reconnect(ctx);
return connection.fake_idle(ctx, Some(watch_folder)).await; return connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await;
} }
if folder_config == Config::ConfiguredInboxFolder { if folder_config == Config::ConfiguredInboxFolder {
@@ -227,7 +252,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
// Fetch the watched folder. // Fetch the watched folder.
if let Err(err) = connection if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false) .fetch_move_delete(ctx, &watch_folder, folder_meaning)
.await .await
.context("fetch_move_delete") .context("fetch_move_delete")
{ {
@@ -265,7 +290,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
// 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 if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false) .fetch_move_delete(ctx, &watch_folder, folder_meaning)
.await .await
.context("fetch_move_delete after scan_folders") .context("fetch_move_delete after scan_folders")
{ {
@@ -293,7 +318,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
ctx, ctx,
"IMAP session does not support IDLE, going to fake idle." "IMAP session does not support IDLE, going to fake idle."
); );
return connection.fake_idle(ctx, Some(watch_folder)).await; return connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await;
} }
info!(ctx, "IMAP session supports IDLE, using it."); info!(ctx, "IMAP session supports IDLE, using it.");
@@ -318,7 +345,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config)
} }
} else { } else {
warn!(ctx, "No IMAP session, going to fake idle."); warn!(ctx, "No IMAP session, going to fake idle.");
connection.fake_idle(ctx, Some(watch_folder)).await connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await
} }
} }
@@ -326,11 +355,11 @@ async fn simple_imap_loop(
ctx: Context, ctx: Context,
started: Sender<()>, started: Sender<()>,
inbox_handlers: ImapConnectionHandlers, inbox_handlers: ImapConnectionHandlers,
folder_config: Config, folder_meaning: FolderMeaning,
) { ) {
use futures::future::FutureExt; use futures::future::FutureExt;
info!(ctx, "starting simple loop for {}", folder_config); info!(ctx, "starting simple loop for {}", folder_meaning);
let ImapConnectionHandlers { let ImapConnectionHandlers {
mut connection, mut connection,
stop_receiver, stop_receiver,
@@ -346,7 +375,7 @@ async fn simple_imap_loop(
} }
loop { loop {
fetch_idle(&ctx, &mut connection, folder_config).await; fetch_idle(&ctx, &mut connection, folder_meaning).await;
} }
}; };
@@ -443,75 +472,56 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
impl Scheduler { impl Scheduler {
/// Start the scheduler. /// Start the scheduler.
pub async fn start(ctx: Context) -> Result<Self> { pub async fn start(ctx: Context) -> Result<Self> {
let (mvbox, mvbox_handlers) = ImapConnectionState::new(&ctx).await?;
let (sentbox, sentbox_handlers) = ImapConnectionState::new(&ctx).await?;
let (smtp, smtp_handlers) = SmtpConnectionState::new(); let (smtp, smtp_handlers) = SmtpConnectionState::new();
let (inbox, inbox_handlers) = ImapConnectionState::new(&ctx).await?;
let (inbox_start_send, inbox_start_recv) = channel::bounded(1);
let (mvbox_start_send, mvbox_start_recv) = channel::bounded(1);
let mut mvbox_handle = None;
let (sentbox_start_send, sentbox_start_recv) = channel::bounded(1);
let mut sentbox_handle = None;
let (smtp_start_send, smtp_start_recv) = channel::bounded(1); let (smtp_start_send, smtp_start_recv) = channel::bounded(1);
let (ephemeral_interrupt_send, ephemeral_interrupt_recv) = channel::bounded(1); let (ephemeral_interrupt_send, ephemeral_interrupt_recv) = channel::bounded(1);
let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1); let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1);
let inbox_handle = { let mut oboxes = Vec::new();
let mut start_recvs = Vec::new();
let (conn_state, inbox_handlers) = ImapConnectionState::new(&ctx).await?;
let (inbox_start_send, inbox_start_recv) = channel::bounded(1);
let handle = {
let ctx = ctx.clone(); let ctx = ctx.clone();
task::spawn(async move { inbox_loop(ctx, inbox_start_send, inbox_handlers).await }) task::spawn(async move { inbox_loop(ctx, inbox_start_send, inbox_handlers).await })
}; };
let inbox = SchedBox {
meaning: FolderMeaning::Inbox,
conn_state,
handle,
};
start_recvs.push(inbox_start_recv);
if ctx.should_watch_mvbox().await? { for (meaning, should_watch) in [
let ctx = ctx.clone(); (FolderMeaning::Mvbox, ctx.should_watch_mvbox().await),
mvbox_handle = Some(task::spawn(async move { (
simple_imap_loop( FolderMeaning::Sent,
ctx, ctx.get_config_bool(Config::SentboxWatch).await,
mvbox_start_send, ),
mvbox_handlers, ] {
Config::ConfiguredMvboxFolder, if should_watch? {
) let (conn_state, handlers) = ImapConnectionState::new(&ctx).await?;
.await let (start_send, start_recv) = channel::bounded(1);
})); let ctx = ctx.clone();
} else { let handle = task::spawn(async move {
mvbox_start_send simple_imap_loop(ctx, start_send, handlers, meaning).await
.send(()) });
.await oboxes.push(SchedBox {
.context("mvbox start send, missing receiver")?; meaning,
mvbox_handlers conn_state,
.connection handle,
.connectivity });
.set_not_configured(&ctx) start_recvs.push(start_recv);
.await }
}
if ctx.get_config_bool(Config::SentboxWatch).await? {
let ctx = ctx.clone();
sentbox_handle = Some(task::spawn(async move {
simple_imap_loop(
ctx,
sentbox_start_send,
sentbox_handlers,
Config::ConfiguredSentboxFolder,
)
.await
}));
} else {
sentbox_start_send
.send(())
.await
.context("sentbox start send, missing receiver")?;
sentbox_handlers
.connection
.connectivity
.set_not_configured(&ctx)
.await
} }
let smtp_handle = { let smtp_handle = {
let ctx = ctx.clone(); let ctx = ctx.clone();
task::spawn(async move { smtp_loop(ctx, smtp_start_send, smtp_handlers).await }) task::spawn(async move { smtp_loop(ctx, smtp_start_send, smtp_handlers).await })
}; };
start_recvs.push(smtp_start_recv);
let ephemeral_handle = { let ephemeral_handle = {
let ctx = ctx.clone(); let ctx = ctx.clone();
@@ -531,12 +541,8 @@ impl Scheduler {
let res = Self { let res = Self {
inbox, inbox,
mvbox, oboxes,
sentbox,
smtp, smtp,
inbox_handle,
mvbox_handle,
sentbox_handle,
smtp_handle, smtp_handle,
ephemeral_handle, ephemeral_handle,
ephemeral_interrupt_send, ephemeral_interrupt_send,
@@ -546,12 +552,7 @@ impl Scheduler {
}; };
// wait for all loops to be started // wait for all loops to be started
if let Err(err) = try_join!( if let Err(err) = try_join_all(start_recvs.iter().map(|r| r.recv())).await {
inbox_start_recv.recv(),
mvbox_start_recv.recv(),
sentbox_start_recv.recv(),
smtp_start_recv.recv()
) {
bail!("failed to start scheduler: {}", err); bail!("failed to start scheduler: {}", err);
} }
@@ -559,30 +560,26 @@ impl Scheduler {
Ok(res) Ok(res)
} }
fn boxes(&self) -> iter::Chain<iter::Once<&SchedBox>, std::slice::Iter<'_, SchedBox>> {
once(&self.inbox).chain(self.oboxes.iter())
}
fn maybe_network(&self) { fn maybe_network(&self) {
self.interrupt_inbox(InterruptInfo::new(true)); for b in self.boxes() {
self.interrupt_mvbox(InterruptInfo::new(true)); b.conn_state.interrupt(InterruptInfo::new(true));
self.interrupt_sentbox(InterruptInfo::new(true)); }
self.interrupt_smtp(InterruptInfo::new(true)); self.interrupt_smtp(InterruptInfo::new(true));
} }
fn maybe_network_lost(&self) { fn maybe_network_lost(&self) {
self.interrupt_inbox(InterruptInfo::new(false)); for b in self.boxes() {
self.interrupt_mvbox(InterruptInfo::new(false)); b.conn_state.interrupt(InterruptInfo::new(false));
self.interrupt_sentbox(InterruptInfo::new(false)); }
self.interrupt_smtp(InterruptInfo::new(false)); self.interrupt_smtp(InterruptInfo::new(false));
} }
fn interrupt_inbox(&self, info: InterruptInfo) { fn interrupt_inbox(&self, info: InterruptInfo) {
self.inbox.interrupt(info); self.inbox.conn_state.interrupt(info);
}
fn interrupt_mvbox(&self, info: InterruptInfo) {
self.mvbox.interrupt(info);
}
fn interrupt_sentbox(&self, info: InterruptInfo) {
self.sentbox.interrupt(info);
} }
fn interrupt_smtp(&self, info: InterruptInfo) { fn interrupt_smtp(&self, info: InterruptInfo) {
@@ -605,29 +602,17 @@ impl Scheduler {
/// ///
/// It consumes the scheduler and never fails to stop it. In the worst case, long-running tasks /// It consumes the scheduler and never fails to stop it. In the worst case, long-running tasks
/// are forcefully terminated if they cannot shutdown within the timeout. /// are forcefully terminated if they cannot shutdown within the timeout.
pub(crate) async fn stop(mut self, context: &Context) { pub(crate) async fn stop(self, context: &Context) {
// Send stop signals to tasks so they can shutdown cleanly. // Send stop signals to tasks so they can shutdown cleanly.
self.inbox.stop().await.ok_or_log(context); for b in self.boxes() {
if self.mvbox_handle.is_some() { b.conn_state.stop().await.ok_or_log(context);
self.mvbox.stop().await.ok_or_log(context);
}
if self.sentbox_handle.is_some() {
self.sentbox.stop().await.ok_or_log(context);
} }
self.smtp.stop().await.ok_or_log(context); self.smtp.stop().await.ok_or_log(context);
// Actually shutdown tasks. // Actually shutdown tasks.
let timeout_duration = std::time::Duration::from_secs(30); let timeout_duration = std::time::Duration::from_secs(30);
tokio::time::timeout(timeout_duration, self.inbox_handle) for b in once(self.inbox).chain(self.oboxes.into_iter()) {
.await tokio::time::timeout(timeout_duration, b.handle)
.ok_or_log(context);
if let Some(mvbox_handle) = self.mvbox_handle.take() {
tokio::time::timeout(timeout_duration, mvbox_handle)
.await
.ok_or_log(context);
}
if let Some(sentbox_handle) = self.sentbox_handle.take() {
tokio::time::timeout(timeout_duration, sentbox_handle)
.await .await
.ok_or_log(context); .ok_or_log(context);
} }

View File

@@ -1,18 +1,18 @@
use core::fmt; use core::fmt;
use std::{ops::Deref, sync::Arc}; use std::{iter::once, ops::Deref, sync::Arc};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use humansize::{format_size, BINARY}; use humansize::{format_size, BINARY};
use tokio::sync::{Mutex, RwLockReadGuard}; use tokio::sync::{Mutex, RwLockReadGuard};
use crate::events::EventType; use crate::events::EventType;
use crate::imap::scan_folders::get_watched_folder_configs; use crate::imap::{scan_folders::get_watched_folder_configs, FolderMeaning};
use crate::quota::{ use crate::quota::{
QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE, QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE,
}; };
use crate::tools::time; use crate::tools::time;
use crate::{config::Config, scheduler::Scheduler, stock_str, tools};
use crate::{context::Context, log::LogExt}; use crate::{context::Context, log::LogExt};
use crate::{scheduler::Scheduler, stock_str, tools};
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
pub enum Connectivity { pub enum Connectivity {
@@ -157,17 +157,14 @@ impl ConnectivityStore {
/// Called during `dc_maybe_network()` to make sure that `dc_accounts_all_work_done()` /// Called during `dc_maybe_network()` to make sure that `dc_accounts_all_work_done()`
/// returns false immediately after `dc_maybe_network()`. /// returns false immediately after `dc_maybe_network()`.
pub(crate) async fn idle_interrupted(scheduler: RwLockReadGuard<'_, Option<Scheduler>>) { pub(crate) async fn idle_interrupted(scheduler: RwLockReadGuard<'_, Option<Scheduler>>) {
let [inbox, mvbox, sentbox] = match &*scheduler { let (inbox, oboxes) = match &*scheduler {
Some(Scheduler { Some(Scheduler { inbox, oboxes, .. }) => (
inbox, inbox.conn_state.state.connectivity.clone(),
mvbox, oboxes
sentbox, .iter()
.. .map(|b| b.conn_state.state.connectivity.clone())
}) => [ .collect::<Vec<_>>(),
inbox.state.connectivity.clone(), ),
mvbox.state.connectivity.clone(),
sentbox.state.connectivity.clone(),
],
None => return, None => return,
}; };
drop(scheduler); drop(scheduler);
@@ -185,7 +182,7 @@ pub(crate) async fn idle_interrupted(scheduler: RwLockReadGuard<'_, Option<Sched
} }
drop(connectivity_lock); drop(connectivity_lock);
for state in &[&mvbox, &sentbox] { for state in oboxes {
let mut connectivity_lock = state.0.lock().await; let mut connectivity_lock = state.0.lock().await;
if *connectivity_lock == DetailedConnectivity::Connected { if *connectivity_lock == DetailedConnectivity::Connected {
*connectivity_lock = DetailedConnectivity::InterruptingIdle; *connectivity_lock = DetailedConnectivity::InterruptingIdle;
@@ -202,17 +199,11 @@ pub(crate) async fn maybe_network_lost(
context: &Context, context: &Context,
scheduler: RwLockReadGuard<'_, Option<Scheduler>>, scheduler: RwLockReadGuard<'_, Option<Scheduler>>,
) { ) {
let stores = match &*scheduler { let stores: Vec<_> = match &*scheduler {
Some(Scheduler { Some(sched) => sched
inbox, .boxes()
mvbox, .map(|b| b.conn_state.state.connectivity.clone())
sentbox, .collect(),
..
}) => [
inbox.state.connectivity.clone(),
mvbox.state.connectivity.clone(),
sentbox.state.connectivity.clone(),
],
None => return, None => return,
}; };
drop(scheduler); drop(scheduler);
@@ -260,14 +251,9 @@ impl Context {
pub async fn get_connectivity(&self) -> Connectivity { pub async fn get_connectivity(&self) -> Connectivity {
let lock = self.scheduler.read().await; let lock = self.scheduler.read().await;
let stores: Vec<_> = match &*lock { let stores: Vec<_> = match &*lock {
Some(Scheduler { Some(sched) => sched
inbox, .boxes()
mvbox, .map(|b| b.conn_state.state.connectivity.clone())
sentbox,
..
}) => [&inbox.state, &mvbox.state, &sentbox.state]
.iter()
.map(|state| state.connectivity.clone())
.collect(), .collect(),
None => return Connectivity::NotConnected, None => return Connectivity::NotConnected,
}; };
@@ -348,28 +334,12 @@ impl Context {
let lock = self.scheduler.read().await; let lock = self.scheduler.read().await;
let (folders_states, smtp) = match &*lock { let (folders_states, smtp) = match &*lock {
Some(Scheduler { Some(sched) => (
inbox, sched
mvbox, .boxes()
sentbox, .map(|b| (b.meaning, b.conn_state.state.connectivity.clone()))
smtp, .collect::<Vec<_>>(),
.. sched.smtp.state.connectivity.clone(),
}) => (
[
(
Config::ConfiguredInboxFolder,
inbox.state.connectivity.clone(),
),
(
Config::ConfiguredMvboxFolder,
mvbox.state.connectivity.clone(),
),
(
Config::ConfiguredSentboxFolder,
sentbox.state.connectivity.clone(),
),
],
smtp.state.connectivity.clone(),
), ),
None => { None => {
return Err(anyhow!("Not started")); return Err(anyhow!("Not started"));
@@ -390,8 +360,8 @@ impl Context {
for (folder, state) in &folders_states { for (folder, state) in &folders_states {
let mut folder_added = false; let mut folder_added = false;
if watched_folders.contains(folder) { if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
let f = self.get_config(*folder).await.ok_or_log(self).flatten(); let f = self.get_config(config).await.ok_or_log(self).flatten();
if let Some(foldername) = f { if let Some(foldername) = f {
let detailed = &state.get_detailed().await; let detailed = &state.get_detailed().await;
@@ -407,7 +377,7 @@ impl Context {
} }
} }
if !folder_added && folder == &Config::ConfiguredInboxFolder { if !folder_added && folder == &FolderMeaning::Inbox {
let detailed = &state.get_detailed().await; let detailed = &state.get_detailed().await;
if let DetailedConnectivity::Error(_) = detailed { if let DetailedConnectivity::Error(_) = detailed {
// On the inbox thread, we also do some other things like scan_folders and run jobs // On the inbox thread, we also do some other things like scan_folders and run jobs
@@ -535,14 +505,10 @@ impl Context {
pub async fn all_work_done(&self) -> bool { pub async fn all_work_done(&self) -> bool {
let lock = self.scheduler.read().await; let lock = self.scheduler.read().await;
let stores: Vec<_> = match &*lock { let stores: Vec<_> = match &*lock {
Some(Scheduler { Some(sched) => sched
inbox, .boxes()
mvbox, .map(|b| &b.conn_state.state)
sentbox, .chain(once(&sched.smtp.state))
smtp,
..
}) => [&inbox.state, &mvbox.state, &sentbox.state, &smtp.state]
.iter()
.map(|state| state.connectivity.clone()) .map(|state| state.connectivity.clone())
.collect(), .collect(),
None => return false, None => return false,