diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 2ab5b40085..3c9c129942 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -430,14 +430,6 @@ char* dc_get_blobdir (const dc_context_t* context); * 1=send a copy of outgoing messages to self (default). * Sending messages to self is needed for a proper multi-account setup, * however, on the other hand, may lead to unwanted notifications in non-delta clients. - * - `mvbox_move` = 1=detect chat messages, - * move them to the `DeltaChat` folder, - * and watch the `DeltaChat` folder for updates (default), - * 0=do not move chat-messages - * - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the - * `DeltaChat` folder. Messages will still be fetched from the - * spam folder. - * 0=watch all folders normally (default) * - `show_emails` = DC_SHOW_EMAILS_OFF (0)= * show direct replies to chats only, * DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)= diff --git a/deltachat-jsonrpc/src/api/types/login_param.rs b/deltachat-jsonrpc/src/api/types/login_param.rs index 6036709cdd..a57f9670c0 100644 --- a/deltachat-jsonrpc/src/api/types/login_param.rs +++ b/deltachat-jsonrpc/src/api/types/login_param.rs @@ -23,6 +23,9 @@ pub struct EnteredLoginParam { /// Imap server port. pub imap_port: Option, + /// IMAP server folder. + pub imap_folder: Option, + /// Imap socket security. pub imap_security: Option, @@ -66,6 +69,7 @@ impl From for EnteredLoginParam { password: param.imap.password, imap_server: param.imap.server.into_option(), imap_port: param.imap.port.into_option(), + imap_folder: param.imap.folder.into_option(), imap_security: imap_security.into_option(), imap_user: param.imap.user.into_option(), smtp_server: param.smtp.server.into_option(), @@ -85,14 +89,15 @@ impl TryFrom for dc::EnteredLoginParam { fn try_from(param: EnteredLoginParam) -> Result { Ok(Self { addr: param.addr, - imap: dc::EnteredServerLoginParam { + imap: dc::EnteredImapLoginParam { server: param.imap_server.unwrap_or_default(), port: param.imap_port.unwrap_or_default(), + folder: param.imap_folder.unwrap_or_default(), security: param.imap_security.unwrap_or_default().into(), user: param.imap_user.unwrap_or_default(), password: param.password, }, - smtp: dc::EnteredServerLoginParam { + smtp: dc::EnteredSmtpLoginParam { server: param.smtp_server.unwrap_or_default(), port: param.smtp_port.unwrap_or_default(), security: param.smtp_security.unwrap_or_default().into(), diff --git a/deltachat-rpc-client/tests/test_folders.py b/deltachat-rpc-client/tests/test_folders.py index d2e60d5d16..3e8a585d89 100644 --- a/deltachat-rpc-client/tests/test_folders.py +++ b/deltachat-rpc-client/tests/test_folders.py @@ -2,32 +2,13 @@ import re import time -import pytest from imap_tools import AND, U from deltachat_rpc_client import Contact, EventType, Message -def test_move_works(acfactory, direct_imap): - ac1, ac2 = acfactory.get_online_accounts(2) - ac2_direct_imap = direct_imap(ac2) - ac2_direct_imap.create_folder("DeltaChat") - ac2.set_config("mvbox_move", "1") - ac2.bring_online() - - chat = ac1.create_chat(ac2) - chat.send_text("message1") - - # Message is moved to the movebox - ac2.wait_for_event(EventType.IMAP_MESSAGE_MOVED) - - # Message is downloaded - msg = ac2.wait_for_incoming_msg().get_snapshot() - assert msg.text == "message1" - - def test_reactions_for_a_reordering_move(acfactory, direct_imap): - """When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command, + """When a batch of messages is moved from Inbox to another folder with a single MOVE command, their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were processed by receive_imf in the wrong order, and, particularly, reactions were processed before messages they refer to and thus dropped. @@ -37,9 +18,6 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap): addr, password = acfactory.get_credentials() ac2 = acfactory.get_unconfigured_account() ac2.add_or_update_transport({"addr": addr, "password": password}) - ac2_direct_imap = direct_imap(ac2) - ac2_direct_imap.create_folder("DeltaChat") - ac2.set_config("mvbox_move", "1") assert ac2.is_configured() ac2.bring_online() @@ -55,11 +33,17 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap): react_str = "\N{THUMBS UP SIGN}" msg1.send_reaction(react_str).wait_until_delivered() - logging.info("moving messages to ac2's DeltaChat folder in the reverse order") + logging.info("moving messages to ac2's movebox folder in the reverse order") ac2_direct_imap = direct_imap(ac2) + ac2_direct_imap.create_folder("Movebox") ac2_direct_imap.connect() for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True): - ac2_direct_imap.conn.move(uid, "DeltaChat") + ac2_direct_imap.conn.move(uid, "Movebox") + + logging.info("moving messages back") + ac2_direct_imap.select_folder("Movebox") + for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()]): + ac2_direct_imap.conn.move(uid, "INBOX") logging.info("receiving messages by ac2") ac2.start_io() @@ -72,25 +56,6 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap): assert list(reactions.reactions_by_contact.values())[0] == [react_str] -def test_move_works_on_self_sent(acfactory, direct_imap): - ac1, ac2 = acfactory.get_online_accounts(2) - - # Create and enable movebox. - ac1_direct_imap = direct_imap(ac1) - ac1_direct_imap.create_folder("DeltaChat") - ac1.set_config("mvbox_move", "1") - ac1.set_config("bcc_self", "1") - ac1.bring_online() - - chat = ac1.create_chat(ac2) - chat.send_text("message1") - ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED) - chat.send_text("message2") - ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED) - chat.send_text("message3") - ac1.wait_for_event(EventType.IMAP_MESSAGE_MOVED) - - def test_moved_markseen(acfactory, direct_imap): """Test that message already moved to DeltaChat folder is marked as seen.""" ac1, ac2 = acfactory.get_online_accounts(2) @@ -131,17 +96,11 @@ def test_moved_markseen(acfactory, direct_imap): assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1 -@pytest.mark.parametrize("mvbox_move", [True, False]) -def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move): +def test_markseen_message_and_mdn(acfactory, direct_imap): ac1, ac2 = acfactory.get_online_accounts(2) for ac in ac1, ac2: ac.set_config("delete_server_after", "0") - if mvbox_move: - ac_direct_imap = direct_imap(ac) - ac_direct_imap.create_folder("DeltaChat") - ac.set_config("mvbox_move", "1") - ac.bring_online() # Do not send BCC to self, we only want to test MDN on ac1. ac1.set_config("bcc_self", "0") @@ -150,10 +109,7 @@ def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move): msg = ac2.wait_for_incoming_msg() msg.mark_seen() - if mvbox_move: - rex = re.compile("Marked messages [0-9]+ in folder DeltaChat as seen.") - else: - rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.") + rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.") for ac in ac1, ac2: while True: @@ -161,12 +117,11 @@ def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move): if event.kind == EventType.INFO and rex.search(event.msg): break - folder = "mvbox" if mvbox_move else "inbox" ac1_direct_imap = direct_imap(ac1) ac2_direct_imap = direct_imap(ac2) - ac1_direct_imap.select_config_folder(folder) - ac2_direct_imap.select_config_folder(folder) + ac1_direct_imap.select_folder("INBOX") + ac2_direct_imap.select_folder("INBOX") # Check that the mdn is marked as seen assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1 diff --git a/deltachat-rpc-client/tests/test_multitransport.py b/deltachat-rpc-client/tests/test_multitransport.py index 7db0318543..03ba4c38bb 100644 --- a/deltachat-rpc-client/tests/test_multitransport.py +++ b/deltachat-rpc-client/tests/test_multitransport.py @@ -9,10 +9,6 @@ def test_add_second_address(acfactory) -> None: account = acfactory.new_configured_account() assert len(account.list_transports()) == 1 - # When the first transport is created, - # mvbox_move and only_fetch_mvbox should be disabled. - assert account.get_config("mvbox_move") == "0" - assert account.get_config("only_fetch_mvbox") == "0" assert account.get_config("show_emails") == "2" qr = acfactory.get_account_qr() @@ -32,32 +28,10 @@ def test_add_second_address(acfactory) -> None: account.delete_transport(second_addr) assert len(account.list_transports()) == 2 - # Enabling mvbox_move or only_fetch_mvbox - # is not allowed when multi-transport is enabled. - for option in ["mvbox_move", "only_fetch_mvbox"]: - with pytest.raises(JsonRpcError): - account.set_config(option, "1") - # show_emails does not matter for multi-relay, can be set to anything account.set_config("show_emails", "0") -@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"]) -def test_no_second_transport_with_mvbox(acfactory, key) -> None: - """Test that second transport cannot be configured if mvbox is used.""" - account = acfactory.new_configured_account() - assert len(account.list_transports()) == 1 - - assert account.get_config("mvbox_move") == "0" - assert account.get_config("only_fetch_mvbox") == "0" - - qr = acfactory.get_account_qr() - account.set_config(key, "1") - - with pytest.raises(JsonRpcError): - account.add_transport_from_qr(qr) - - def test_second_transport_without_classic_emails(acfactory) -> None: """Test that second transport can be configured if classic emails are not fetched.""" account = acfactory.new_configured_account() @@ -147,44 +121,13 @@ def test_download_on_demand(acfactory) -> None: assert msg.get_snapshot().download_state == dstate -@pytest.mark.parametrize("is_chatmail", ["0", "1"]) -def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None: - """Test that mvbox_move is disabled by default even for non-chatmail accounts. - Disabling mvbox_move is required to be able to setup a second transport. - """ - account = acfactory.get_unconfigured_account() - - account.set_config("fix_is_chatmail", "1") - account.set_config("is_chatmail", is_chatmail) - - # The default value when the setting is unset is "1". - # This is not changed for compatibility with old databases - # imported from backups. - assert account.get_config("mvbox_move") == "1" - - qr = acfactory.get_account_qr() - account.add_transport_from_qr(qr) - - # Once the first transport is set up, - # mvbox_move is disabled. - assert account.get_config("mvbox_move") == "0" - assert account.get_config("is_chatmail") == is_chatmail - - def test_reconfigure_transport(acfactory) -> None: - """Test that reconfiguring the transport works - even if settings not supported for multi-transport - like mvbox_move are enabled.""" + """Test that reconfiguring the transport works.""" account = acfactory.get_online_account() - account.set_config("mvbox_move", "1") [transport] = account.list_transports() account.add_or_update_transport(transport) - # Reconfiguring the transport should not reset - # the settings as if when configuring the first transport. - assert account.get_config("mvbox_move") == "1" - def test_transport_synchronization(acfactory, log) -> None: """Test synchronization of transports between devices.""" diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index e658789ef2..d9b64a8b9b 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -522,7 +522,6 @@ def prepare_account_from_liveconfig(self, configdict) -> Account: ac = self.get_unconfigured_account() assert "addr" in configdict and "mail_pw" in configdict, configdict configdict.setdefault("bcc_self", False) - configdict.setdefault("mvbox_move", False) configdict.setdefault("sync_msgs", False) configdict.setdefault("delete_server_after", 0) ac.update_config(configdict) diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 34ab1cc2da..3ffc52830a 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -52,19 +52,19 @@ def test_wrong_config_keys(self, acfactory): def test_set_config_int_conversion(self, acfactory): ac1 = acfactory.get_unconfigured_account() - ac1.set_config("mvbox_move", False) - assert ac1.get_config("mvbox_move") == "0" - ac1.set_config("mvbox_move", True) - assert ac1.get_config("mvbox_move") == "1" - ac1.set_config("mvbox_move", 0) - assert ac1.get_config("mvbox_move") == "0" - ac1.set_config("mvbox_move", 1) - assert ac1.get_config("mvbox_move") == "1" + ac1.set_config("bcc_self", False) + assert ac1.get_config("bcc_self") == "0" + ac1.set_config("bcc_self", True) + assert ac1.get_config("bcc_self") == "1" + ac1.set_config("bcc_self", 0) + assert ac1.get_config("bcc_self") == "0" + ac1.set_config("bcc_self", 1) + assert ac1.get_config("bcc_self") == "1" def test_update_config(self, acfactory): ac1 = acfactory.get_unconfigured_account() - ac1.update_config({"mvbox_move": False}) - assert ac1.get_config("mvbox_move") == "0" + ac1.update_config({"bcc_self": True}) + assert ac1.get_config("bcc_self") == "1" def test_has_bccself(self, acfactory): ac1 = acfactory.get_unconfigured_account() diff --git a/src/config.rs b/src/config.rs index adba3f610f..305bf441a3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -155,18 +155,6 @@ pub enum Config { #[strum(props(default = "1"))] MdnsEnabled, - /// True if chat messages should be moved to a separate folder. Auto-sent messages like sync - /// ones are moved there anyway. - #[strum(props(default = "1"))] - MvboxMove, - - /// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only. - /// - /// This will not entirely disable other folders, e.g. the spam folder will also still - /// be watched for new messages. - #[strum(props(default = "0"))] - OnlyFetchMvbox, - /// Whether to show classic emails or only chat messages. #[strum(props(default = "2"))] // also change ShowEmails.default() on changes ShowEmails, @@ -268,9 +256,6 @@ pub enum Config { /// Configured folder for incoming messages. ConfiguredInboxFolder, - /// Configured folder for chat messages. - ConfiguredMvboxFolder, - /// Unix timestamp of the last successful configuration. ConfiguredTimestamp, @@ -467,7 +452,6 @@ impl Config { self, Self::Displayname | Self::MdnsEnabled - | Self::MvboxMove | Self::ShowEmails | Self::Selfavatar | Self::Selfstatus, @@ -476,10 +460,7 @@ impl Config { /// Whether the config option needs an IO scheduler restart to take effect. pub(crate) fn needs_io_restart(&self) -> bool { - matches!( - self, - Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr - ) + matches!(self, Config::ConfiguredAddr) } } @@ -594,13 +575,6 @@ impl Context { .is_some_and(|x| x != 0)) } - /// Returns true if movebox ("DeltaChat" folder) should be watched. - pub(crate) async fn should_watch_mvbox(&self) -> Result { - Ok(self.get_config_bool(Config::MvboxMove).await? - || self.get_config_bool(Config::OnlyFetchMvbox).await? - || !self.get_config_bool(Config::IsChatmail).await?) - } - /// Returns true if sync messages should be sent. pub(crate) async fn should_send_sync_msgs(&self) -> Result { Ok(self.get_config_bool(Config::SyncMsgs).await? @@ -682,8 +656,6 @@ impl Context { | Config::ProxyEnabled | Config::BccSelf | Config::MdnsEnabled - | Config::MvboxMove - | Config::OnlyFetchMvbox | Config::Configured | Config::Bot | Config::NotifyAboutWrongPw @@ -706,11 +678,6 @@ impl Context { pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> { Self::check_config(key, value)?; - let n_transports = self.count_transports().await?; - if n_transports > 1 && matches!(key, Config::MvboxMove | Config::OnlyFetchMvbox) { - bail!("Cannot reconfigure {key} when multiple transports are configured"); - } - let _pause = match key.needs_io_restart() { true => self.scheduler.pause(self).await?, _ => Default::default(), @@ -789,12 +756,6 @@ impl Context { .set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref()) .await?; } - Config::MvboxMove => { - self.sql.set_raw_config(key.as_ref(), value).await?; - self.sql - .set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None) - .await?; - } Config::ConfiguredAddr => { let Some(addr) = value else { bail!("Cannot unset configured_addr"); diff --git a/src/config/config_tests.rs b/src/config/config_tests.rs index 9a1a8f2c55..0952ae8949 100644 --- a/src/config/config_tests.rs +++ b/src/config/config_tests.rs @@ -196,11 +196,11 @@ async fn test_sync() -> Result<()> { sync(&alice0, &alice1).await; assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false); - for key in [Config::ShowEmails, Config::MvboxMove] { - let val = alice0.get_config_bool(key).await?; - alice0.set_config_bool(key, !val).await?; + { + let val = alice0.get_config_bool(Config::ShowEmails).await?; + alice0.set_config_bool(Config::ShowEmails, !val).await?; sync(&alice0, &alice1).await; - assert_eq!(alice1.get_config_bool(key).await?, !val); + assert_eq!(alice1.get_config_bool(Config::ShowEmails).await?, !val); } // `Config::SyncMsgs` mustn't be synced. diff --git a/src/configure.rs b/src/configure.rs index 5040aac6f4..041c31042b 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -273,31 +273,16 @@ impl Context { (¶m.addr,), ) .await? - { - // Should be checked before `MvboxMove` because the latter makes no sense in presense of - // `OnlyFetchMvbox` and even grayed out in the UIs in this case. - if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") { - bail!( - "To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"." - ); - } - if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") { - bail!( - "To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"." - ); - } - - if self + && self .sql .count("SELECT COUNT(*) FROM transports", ()) .await? >= MAX_TRANSPORT_RELAYS - { - bail!( - "You have reached the maximum number of relays ({}).", - MAX_TRANSPORT_RELAYS - ) - } + { + bail!( + "You have reached the maximum number of relays ({}).", + MAX_TRANSPORT_RELAYS + ) } let provider = match configure(self, param).await { @@ -405,6 +390,7 @@ async fn get_configured_param( && param.imap.port == 0 && param.imap.security == Socket::Automatic && param.imap.user.is_empty() + && param.imap.folder.is_empty() && param.smtp.server.is_empty() && param.smtp.port == 0 && param.smtp.security == Socket::Automatic @@ -510,6 +496,7 @@ async fn get_configured_param( .collect(), imap_user: param.imap.user.clone(), imap_password: param.imap.password.clone(), + imap_folder: Some(param.imap.folder.clone()).filter(|folder| !folder.is_empty()), smtp: servers .iter() .filter_map(|params| { @@ -602,14 +589,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result format!(""), }; - let mvbox_move = self.get_config_int(Config::MvboxMove).await?; - let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?; - let folders_configured = self - .sql - .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY) - .await? - .unwrap_or_default(); - - let configured_inbox_folder = self - .get_config(Config::ConfiguredInboxFolder) - .await? - .unwrap_or_else(|| "".to_string()); - let configured_mvbox_folder = self - .get_config(Config::ConfiguredMvboxFolder) - .await? - .unwrap_or_else(|| "".to_string()); - let mut res = get_info(); // insert values @@ -956,14 +932,6 @@ impl Context { .await? .to_string(), ); - res.insert("mvbox_move", mvbox_move.to_string()); - res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string()); - res.insert( - constants::DC_FOLDERS_CONFIGURED_KEY, - folders_configured.to_string(), - ); - res.insert("configured_inbox_folder", configured_inbox_folder); - res.insert("configured_mvbox_folder", configured_mvbox_folder); res.insert("mdns_enabled", mdns_enabled.to_string()); res.insert("bcc_self", bcc_self.to_string()); res.insert("sync_msgs", sync_msgs.to_string()); @@ -1263,12 +1231,6 @@ ORDER BY m.timestamp DESC,m.id DESC", Ok(list) } - /// Returns true if given folder name is the name of the "DeltaChat" folder. - pub async fn is_mvbox(&self, folder_name: &str) -> Result { - let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?; - Ok(mvbox.as_deref() == Some(folder_name)) - } - pub(crate) fn derive_blobdir(dbfile: &Path) -> PathBuf { let mut blob_fname = OsString::new(); blob_fname.push(dbfile.file_name().unwrap_or_default()); diff --git a/src/imap.rs b/src/imap.rs index 430ce4420a..c528316a83 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -28,9 +28,10 @@ use crate::calls::{ use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg}; use crate::chatlist_events; use crate::config::Config; -use crate::constants::{self, Blocked, DC_VERSION_STR, ShowEmails}; +use crate::constants::{Blocked, DC_VERSION_STR, ShowEmails}; use crate::contact::ContactId; use crate::context::Context; +use crate::ensure_and_debug_assert; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::log::{LogExt, warn}; @@ -92,6 +93,9 @@ pub(crate) struct Imap { oauth2: bool, + /// Watched folder. + pub(crate) folder: String, + authentication_failed_once: bool, pub(crate) connectivity: ConnectivityStore, @@ -163,7 +167,6 @@ pub enum FolderMeaning { /// Spam folder. Spam, Inbox, - Mvbox, Trash, /// Virtual folders. @@ -175,19 +178,6 @@ pub enum FolderMeaning { Virtual, } -impl FolderMeaning { - pub fn to_config(self) -> Option { - match self { - FolderMeaning::Unknown => None, - FolderMeaning::Spam => None, - FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder), - FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder), - FolderMeaning::Trash => None, - FolderMeaning::Virtual => None, - } - } -} - struct UidGrouper> { inner: Peekable, } @@ -263,6 +253,11 @@ impl Imap { let addr = ¶m.addr; let strict_tls = param.strict_tls(proxy_config.is_some()); let oauth2 = param.oauth2; + let folder = param + .imap_folder + .clone() + .unwrap_or_else(|| "INBOX".to_string()); + ensure_and_debug_assert!(!folder.is_empty(), "Watched folder name cannot be empty"); let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1); Ok(Imap { transport_id, @@ -273,6 +268,7 @@ impl Imap { proxy_config, strict_tls, oauth2, + folder, authentication_failed_once: false, connectivity: Default::default(), conn_last_try: UNIX_EPOCH, @@ -485,7 +481,7 @@ impl Imap { /// that folders are created and IMAP capabilities are determined. pub(crate) async fn prepare(&mut self, context: &Context) -> Result { let configuring = false; - let mut session = match self.connect(context, configuring).await { + let session = match self.connect(context, configuring).await { Ok(session) => session, Err(err) => { self.connectivity.set_err(context, &err); @@ -493,14 +489,6 @@ impl Imap { } }; - 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 { - self.configure_folders(context, &mut session).await?; - } - Ok(session) } @@ -513,15 +501,15 @@ impl Imap { context: &Context, session: &mut Session, watch_folder: &str, - folder_meaning: FolderMeaning, ) -> Result<()> { + ensure_and_debug_assert!(!watch_folder.is_empty(), "Watched folder cannot be empty"); if !context.sql.is_open().await { // probably shutdown bail!("IMAP operation attempted while it is torn down"); } let msgs_fetched = self - .fetch_new_messages(context, session, watch_folder, folder_meaning) + .fetch_new_messages(context, session, watch_folder) .await .context("fetch_new_messages")?; if msgs_fetched && context.get_config_delete_device_after().await?.is_some() { @@ -548,14 +536,7 @@ impl Imap { context: &Context, session: &mut Session, folder: &str, - folder_meaning: FolderMeaning, ) -> Result { - if should_ignore_folder(context, folder, folder_meaning).await? { - info!(context, "Not fetching from {folder:?}."); - session.new_mail = false; - return Ok(false); - } - let folder_exists = session .select_with_uidvalidity(context, folder) .await @@ -572,9 +553,7 @@ impl Imap { let mut read_cnt = 0; loop { - let (n, fetch_more) = self - .fetch_new_msg_batch(context, session, folder, folder_meaning) - .await?; + let (n, fetch_more) = self.fetch_new_msg_batch(context, session, folder).await?; read_cnt += n; if !fetch_more { return Ok(read_cnt > 0); @@ -588,7 +567,6 @@ impl Imap { context: &Context, session: &mut Session, folder: &str, - folder_meaning: FolderMeaning, ) -> Result<(usize, bool)> { let transport_id = self.transport_id; let uid_validity = get_uidvalidity(context, transport_id, folder).await?; @@ -657,13 +635,7 @@ impl Imap { info!(context, "Deleting locally deleted message {message_id}."); } - let _target; - let target = if delete { - "" - } else { - _target = target_folder(context, folder, folder_meaning, &headers).await?; - &_target - }; + let target = if delete { "" } else { folder }; context .sql @@ -691,18 +663,9 @@ impl Imap { // message, move it to the movebox and then download the second message before // downloading the first one, if downloading from inbox before moving is allowed. if folder == target - // Never download messages directly from the spam folder. - // If the sender is known, the message will be moved to the Inbox or Mvbox - // and then we download the message from there. - // Also see `spam_target_folder_cfg()`. - && folder_meaning != FolderMeaning::Spam - && prefetch_should_download( - context, - &headers, - &message_id, - fetch_response.flags(), - ) - .await.context("prefetch_should_download")? + && prefetch_should_download(context, &headers, &message_id, fetch_response.flags()) + .await + .context("prefetch_should_download")? { if headers .get_header_value(HeaderDef::ChatIsPostMessage) @@ -1616,13 +1579,8 @@ impl Session { // Store new encrypted device token on the server // even if it is the same as the old one. if let Some(encrypted_device_token) = new_encrypted_device_token { - let folder = context - .get_config(Config::ConfiguredInboxFolder) - .await? - .context("INBOX is not configured")?; - self.run_command_and_check_ok(&format_setmetadata( - &folder, + "INBOX", &encrypted_device_token, )) .await @@ -1667,117 +1625,6 @@ impl Session { } Ok(()) } - - /// Attempts to configure mvbox. - /// - /// Tries to find any folder examining `folders` in the order they go. - /// This method does not use LIST command to ensure that - /// configuration works even if mailbox lookup is forbidden via Access Control List (see - /// ). - /// - /// Returns first found folder name. - async fn configure_mvbox<'a>( - &mut self, - context: &Context, - folders: &[&'a str], - ) -> Result> { - // Close currently selected folder if needed. - // We are going to select folders using low-level EXAMINE operations below. - self.maybe_close_folder(context).await?; - - for folder in folders { - info!(context, "Looking for MVBOX-folder \"{}\"...", &folder); - let res = self.examine(&folder).await; - if res.is_ok() { - info!( - context, - "MVBOX-folder {:?} successfully selected, using it.", &folder - ); - self.close().await?; - // 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. - let folder_exists = self.select_with_uidvalidity(context, folder).await?; - ensure!(folder_exists, "No MVBOX folder {:?}??", &folder); - return Ok(Some(folder)); - } - } - - Ok(None) - } -} - -impl Imap { - pub(crate) async fn configure_folders( - &mut self, - context: &Context, - session: &mut Session, - ) -> Result<()> { - let mut folders = session - .list(Some(""), Some("*")) - .await - .context("list_folders failed")?; - let mut delimiter = ".".to_string(); - let mut delimiter_is_default = true; - let mut folder_configs = BTreeMap::new(); - - while let Some(folder) = folders.try_next().await? { - info!(context, "Scanning folder: {:?}", folder); - - // Update the delimiter iff there is a different one, but only once. - if let Some(d) = folder.delimiter() - && delimiter_is_default - && !d.is_empty() - && delimiter != d - { - delimiter = d.to_string(); - delimiter_is_default = false; - } - - let folder_meaning = get_folder_meaning_by_attrs(folder.attributes()); - 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()); - } - } - drop(folders); - - info!(context, "Using \"{}\" as folder-delimiter.", delimiter); - - let fallback_folder = format!("INBOX{delimiter}DeltaChat"); - let mvbox_folder = session - .configure_mvbox(context, &["DeltaChat", &fallback_folder]) - .await - .context("failed to configure mvbox")?; - - context - .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX")) - .await?; - if let Some(mvbox_folder) = mvbox_folder { - info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder); - context - .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder)) - .await?; - } - for (config, name) in folder_configs { - context.set_config_internal(config, Some(&name)).await?; - } - context - .sql - .set_raw_config_int( - constants::DC_FOLDERS_CONFIGURED_KEY, - constants::DC_FOLDERS_CONFIGURED_VERSION, - ) - .await?; - - info!(context, "FINISHED configuring IMAP-folders."); - Ok(()) - } } impl Session { @@ -1911,15 +1758,7 @@ async fn spam_target_folder_cfg( return Ok(None); } - if needs_move_to_mvbox(context, headers).await? - // If OnlyFetchMvbox is set, we don't want to move the message to - // the inbox where we wouldn't fetch it again: - || context.get_config_bool(Config::OnlyFetchMvbox).await? - { - Ok(Some(Config::ConfiguredMvboxFolder)) - } else { - Ok(Some(Config::ConfiguredInboxFolder)) - } + Ok(Some(Config::ConfiguredInboxFolder)) } /// Returns `ConfiguredInboxFolder` or `ConfiguredMvboxFolder` if @@ -1930,16 +1769,12 @@ pub async fn target_folder_cfg( folder_meaning: FolderMeaning, headers: &[mailparse::MailHeader<'_>], ) -> Result> { - if context.is_mvbox(folder).await? { + if folder == "DeltaChat" { return Ok(None); } if folder_meaning == FolderMeaning::Spam { spam_target_folder_cfg(context, headers).await - } else if folder_meaning == FolderMeaning::Inbox - && needs_move_to_mvbox(context, headers).await? - { - Ok(Some(Config::ConfiguredMvboxFolder)) } else { Ok(None) } @@ -1960,36 +1795,6 @@ pub async fn target_folder( } } -async fn needs_move_to_mvbox( - context: &Context, - headers: &[mailparse::MailHeader<'_>], -) -> Result { - let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some(); - if !context.get_config_bool(Config::MvboxMove).await? { - return Ok(false); - } - - if headers - .get_header_value(HeaderDef::AutocryptSetupMessage) - .is_some() - { - // do not move setup messages; - // there may be a non-delta device that wants to handle it - return Ok(false); - } - - if has_chat_version { - Ok(true) - } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? { - match parent.is_dc_message { - MessengerMessage::No => Ok(false), - MessengerMessage::Yes | MessengerMessage::Reply => Ok(true), - } - } else { - Ok(false) - } -} - /// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST. // TODO: lots languages missing - maybe there is a list somewhere on other MUAs? // however, if we fail to find out the sent-folder, @@ -2346,21 +2151,6 @@ async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Resul .unwrap_or(0)) } -/// Whether to ignore fetching messages from a folder. -/// -/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders -/// not explicitly watched should not be fetched. -async fn should_ignore_folder( - context: &Context, - folder: &str, - folder_meaning: FolderMeaning, -) -> Result { - if !context.get_config_bool(Config::OnlyFetchMvbox).await? { - return Ok(false); - } - 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 /// characters because according to /// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars) @@ -2419,23 +2209,5 @@ impl std::fmt::Display for UidRange { } } -pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result> { - let mut res = vec![Config::ConfiguredInboxFolder]; - if context.should_watch_mvbox().await? { - res.push(Config::ConfiguredMvboxFolder); - } - Ok(res) -} - -pub(crate) async fn get_watched_folders(context: &Context) -> Result> { - 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) -} - #[cfg(test)] mod imap_tests; diff --git a/src/imap/idle.rs b/src/imap/idle.rs index 0a5217a615..850a33dfc7 100644 --- a/src/imap/idle.rs +++ b/src/imap/idle.rs @@ -115,11 +115,7 @@ impl Session { impl Imap { /// Idle using polling. - pub(crate) async fn fake_idle( - &mut self, - context: &Context, - watch_folder: String, - ) -> Result<()> { + pub(crate) async fn fake_idle(&mut self, context: &Context, watch_folder: &str) -> Result<()> { let fake_idle_start_time = tools::Time::now(); info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder); diff --git a/src/imap/imap_tests.rs b/src/imap/imap_tests.rs index 4ba3142133..8755b14de9 100644 --- a/src/imap/imap_tests.rs +++ b/src/imap/imap_tests.rs @@ -100,7 +100,6 @@ fn test_build_sequence_sets() { async fn check_target_folder_combination( folder: &str, - mvbox_move: bool, chat_msg: bool, expected_destination: &str, accepted_chat: bool, @@ -108,16 +107,10 @@ async fn check_target_folder_combination( setupmessage: bool, ) -> Result<()> { println!( - "Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}" + "Testing: For folder {folder}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}" ); let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat")) - .await?; - t.ctx - .set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" })) - .await?; if accepted_chat { let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?; @@ -164,42 +157,33 @@ async fn check_target_folder_combination( assert_eq!( expected, actual.as_deref(), - "For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}" + "For folder {folder}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}" ); Ok(()) } // chat_msg means that the message was sent by Delta Chat -// The tuples are (folder, mvbox_move, chat_msg, expected_destination) -const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[ - ("INBOX", false, false, "INBOX"), - ("INBOX", false, true, "INBOX"), - ("INBOX", true, false, "INBOX"), - ("INBOX", true, true, "DeltaChat"), - ("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs - ("Spam", false, true, "INBOX"), - ("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs - ("Spam", true, true, "DeltaChat"), +// The tuples are (folder, chat_msg, expected_destination) +const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, &str)] = &[ + ("INBOX", false, "INBOX"), + ("INBOX", true, "INBOX"), + ("Spam", false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs + ("Spam", true, "INBOX"), ]; // These are the same as above, but non-chat messages in Spam stay in Spam -const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[ - ("INBOX", false, false, "INBOX"), - ("INBOX", false, true, "INBOX"), - ("INBOX", true, false, "INBOX"), - ("INBOX", true, true, "DeltaChat"), - ("Spam", false, false, "Spam"), - ("Spam", false, true, "INBOX"), - ("Spam", true, false, "Spam"), - ("Spam", true, true, "DeltaChat"), +const COMBINATIONS_REQUEST: &[(&str, bool, &str)] = &[ + ("INBOX", false, "INBOX"), + ("INBOX", true, "INBOX"), + ("Spam", false, "Spam"), + ("Spam", true, "INBOX"), ]; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_target_folder_incoming_accepted() -> Result<()> { - for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT { + for (folder, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT { check_target_folder_combination( folder, - *mvbox_move, *chat_msg, expected_destination, true, @@ -213,10 +197,9 @@ async fn test_target_folder_incoming_accepted() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_target_folder_incoming_request() -> Result<()> { - for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST { + for (folder, chat_msg, expected_destination) in COMBINATIONS_REQUEST { check_target_folder_combination( folder, - *mvbox_move, *chat_msg, expected_destination, false, @@ -231,17 +214,9 @@ async fn test_target_folder_incoming_request() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_target_folder_outgoing() -> Result<()> { // Test outgoing emails - for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT { - check_target_folder_combination( - folder, - *mvbox_move, - *chat_msg, - expected_destination, - true, - true, - false, - ) - .await?; + for (folder, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT { + check_target_folder_combination(folder, *chat_msg, expected_destination, true, true, false) + .await?; } Ok(()) } @@ -249,10 +224,9 @@ async fn test_target_folder_outgoing() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_target_folder_setupmsg() -> Result<()> { // Test setupmessages - for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT { + for (folder, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT { check_target_folder_combination( folder, - *mvbox_move, *chat_msg, if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam" false, diff --git a/src/login_param.rs b/src/login_param.rs index 5c6864f118..2af180e159 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -56,9 +56,37 @@ pub enum EnteredCertificateChecks { AcceptInvalidCertificates2 = 3, } -/// Login parameters for a single server, either IMAP or SMTP +/// Login parameters for a single IMAP server. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct EnteredServerLoginParam { +pub struct EnteredImapLoginParam { + /// Server hostname or IP address. + pub server: String, + + /// Server port. + /// + /// 0 if not specified. + pub port: u16, + + /// Folder to watch. + /// + /// If empty, user has not entered anything and it shuold expand to "INBOX" later. + pub folder: String, + + /// Socket security. + pub security: Socket, + + /// Username. + /// + /// Empty string if not specified. + pub user: String, + + /// Password. + pub password: String, +} + +/// Login parameters for a single SMTP server. +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EnteredSmtpLoginParam { /// Server hostname or IP address. pub server: String, @@ -86,10 +114,10 @@ pub struct EnteredLoginParam { pub addr: String, /// IMAP settings. - pub imap: EnteredServerLoginParam, + pub imap: EnteredImapLoginParam, /// SMTP settings. - pub smtp: EnteredServerLoginParam, + pub smtp: EnteredSmtpLoginParam, /// TLS options: whether to allow invalid certificates and/or /// invalid hostnames @@ -101,6 +129,8 @@ pub struct EnteredLoginParam { impl EnteredLoginParam { /// Loads entered account settings. + /// + /// This is a legacy API for loading from separate config parameters. pub(crate) async fn load(context: &Context) -> Result { let addr = context .get_config(Config::Addr) @@ -117,6 +147,10 @@ impl EnteredLoginParam { .get_config_parsed::(Config::MailPort) .await? .unwrap_or_default(); + + // There is no way to set custom folder with this legacy API. + let mail_folder = String::new(); + let mail_security = context .get_config_parsed::(Config::MailSecurity) .await? @@ -175,14 +209,15 @@ impl EnteredLoginParam { Ok(EnteredLoginParam { addr, - imap: EnteredServerLoginParam { + imap: EnteredImapLoginParam { server: mail_server, port: mail_port, + folder: mail_folder, security: mail_security, user: mail_user, password: mail_pw, }, - smtp: EnteredServerLoginParam { + smtp: EnteredSmtpLoginParam { server: send_server, port: send_port, security: send_security, @@ -344,14 +379,15 @@ mod tests { let t = TestContext::new().await; let param = EnteredLoginParam { addr: "alice@example.org".to_string(), - imap: EnteredServerLoginParam { + imap: EnteredImapLoginParam { server: "".to_string(), port: 0, + folder: "".to_string(), security: Socket::Starttls, user: "".to_string(), password: "foobar".to_string(), }, - smtp: EnteredServerLoginParam { + smtp: EnteredSmtpLoginParam { server: "".to_string(), port: 2947, security: Socket::default(), diff --git a/src/qr.rs b/src/qr.rs index a289d9aafa..3454328a5e 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -17,7 +17,7 @@ use crate::config::Config; use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::key::Fingerprint; -use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam}; +use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam, EnteredLoginParam}; use crate::net::http::post_empty; use crate::net::proxy::{DEFAULT_SOCKS_PORT, ProxyConfig}; use crate::token; @@ -789,7 +789,7 @@ pub(crate) async fn login_param_from_account_qr( let param = EnteredLoginParam { addr, - imap: EnteredServerLoginParam { + imap: EnteredImapLoginParam { password, ..Default::default() }, @@ -809,7 +809,7 @@ pub(crate) async fn login_param_from_account_qr( let param = EnteredLoginParam { addr: email, - imap: EnteredServerLoginParam { + imap: EnteredImapLoginParam { password, ..Default::default() }, diff --git a/src/qr/dclogin_scheme.rs b/src/qr/dclogin_scheme.rs index 4059ee0ab2..276fec7449 100644 --- a/src/qr/dclogin_scheme.rs +++ b/src/qr/dclogin_scheme.rs @@ -5,7 +5,9 @@ use anyhow::{Context as _, Result, bail}; use deltachat_contact_tools::may_be_valid_addr; use super::{DCLOGIN_SCHEME, Qr}; -use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam}; +use crate::login_param::{ + EnteredCertificateChecks, EnteredImapLoginParam, EnteredLoginParam, EnteredSmtpLoginParam, +}; use crate::provider::Socket; /// Options for `dclogin:` scheme. @@ -173,14 +175,15 @@ pub(crate) fn login_param_from_login_qr( } => { let param = EnteredLoginParam { addr: addr.to_string(), - imap: EnteredServerLoginParam { + imap: EnteredImapLoginParam { server: imap_host.unwrap_or_default(), port: imap_port.unwrap_or_default(), + folder: "INBOX".to_string(), security: imap_security.unwrap_or_default(), user: imap_username.unwrap_or_default(), password: imap_password.unwrap_or(mail_pw), }, - smtp: EnteredServerLoginParam { + smtp: EnteredSmtpLoginParam { server: smtp_host.unwrap_or_default(), port: smtp_port.unwrap_or_default(), security: smtp_security.unwrap_or_default(), diff --git a/src/quota.rs b/src/quota.rs index ba6adbf017..24c825d3d7 100644 --- a/src/quota.rs +++ b/src/quota.rs @@ -9,7 +9,6 @@ use async_imap::types::{Quota, QuotaResource}; use crate::chat::add_device_msg_with_importance; use crate::config::Config; use crate::context::Context; -use crate::imap::get_watched_folders; use crate::imap::session::Session as ImapSession; use crate::log::warn; use crate::message::Message; @@ -48,26 +47,24 @@ pub struct QuotaInfo { async fn get_unique_quota_roots_and_usage( session: &mut ImapSession, - folders: Vec, + folder: String, ) -> Result>> { let mut unique_quota_roots: BTreeMap> = BTreeMap::new(); - for folder in folders { - let (quota_roots, quotas) = &session.get_quota_root(&folder).await?; - // if there are new quota roots found in this imap folder, add them to the list - for qr_entries in quota_roots { - for quota_root_name in &qr_entries.quota_root_names { - // the quota for that quota root - let quota: Quota = quotas - .iter() - .find(|q| &q.root_name == quota_root_name) - .cloned() - .context("quota_root should have a quota")?; - // replace old quotas, because between fetching quotaroots for folders, - // messages could be received and so the usage could have been changed - *unique_quota_roots - .entry(quota_root_name.clone()) - .or_default() = quota.resources; - } + let (quota_roots, quotas) = &session.get_quota_root(&folder).await?; + // if there are new quota roots found in this imap folder, add them to the list + for qr_entries in quota_roots { + for quota_root_name in &qr_entries.quota_root_names { + // the quota for that quota root + let quota: Quota = quotas + .iter() + .find(|q| &q.root_name == quota_root_name) + .cloned() + .context("quota_root should have a quota")?; + // replace old quotas, because between fetching quotaroots for folders, + // messages could be received and so the usage could have been changed + *unique_quota_roots + .entry(quota_root_name.clone()) + .or_default() = quota.resources; } } Ok(unique_quota_roots) @@ -123,10 +120,13 @@ impl Context { /// As the message is added only once, the user is not spammed /// in case for some providers the quota is always at ~100% /// and new space is allocated as needed. - pub(crate) async fn update_recent_quota(&self, session: &mut ImapSession) -> Result<()> { + pub(crate) async fn update_recent_quota( + &self, + session: &mut ImapSession, + folder: String, + ) -> Result<()> { let quota = if session.can_check_quota() { - let folders = get_watched_folders(self).await?; - get_unique_quota_roots_and_usage(session, folders).await + get_unique_quota_roots_and_usage(session, folder).await } else { Err(anyhow!(stock_str::not_supported_by_provider(self).await)) }; diff --git a/src/scheduler.rs b/src/scheduler.rs index b69d360e36..79a51b6c24 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -17,7 +17,7 @@ use crate::context::Context; use crate::download::{download_known_post_messages_without_pre_message, download_msgs}; use crate::ephemeral::{self, delete_expired_imap_messages}; use crate::events::EventType; -use crate::imap::{FolderMeaning, Imap, session::Session}; +use crate::imap::{Imap, session::Session}; use crate::location; use crate::log::{LogExt, warn}; use crate::smtp::{Smtp, send_smtp_messages}; @@ -211,25 +211,19 @@ impl SchedulerState { /// Indicate that the network likely has come back. pub(crate) async fn maybe_network(&self) { let inner = self.inner.read().await; - let (inboxes, oboxes) = match *inner { + let inboxes = match *inner { InnerSchedulerState::Started(ref scheduler) => { scheduler.maybe_network(); - let inboxes = scheduler + scheduler .inboxes .iter() .map(|b| b.conn_state.state.connectivity.clone()) - .collect::>(); - let oboxes = scheduler - .oboxes - .iter() - .map(|b| b.conn_state.state.connectivity.clone()) - .collect::>(); - (inboxes, oboxes) + .collect::>() } _ => return, }; drop(inner); - connectivity::idle_interrupted(inboxes, oboxes); + connectivity::idle_interrupted(inboxes); } /// Indicate that the network likely is lost. @@ -318,7 +312,10 @@ impl Drop for IoPausedGuard { struct SchedBox { /// Address at the used chatmail/email relay addr: String, - meaning: FolderMeaning, + + /// Folder name + folder: String, + conn_state: ImapConnectionState, /// IMAP loop task handle. @@ -330,8 +327,6 @@ struct SchedBox { pub(crate) struct Scheduler { /// Inboxes, one per transport. inboxes: Vec, - /// Optional boxes -- mvbox. - oboxes: Vec, smtp: SmtpConnectionState, smtp_handle: task::JoinHandle<()>, ephemeral_handle: task::JoinHandle<()>, @@ -400,36 +395,6 @@ async fn inbox_loop( .await; } -/// Convert folder meaning -/// used internally by [fetch_idle] and [Context::background_fetch]. -/// -/// Returns folder configuration key and folder name -/// if such folder is configured, `Ok(None)` otherwise. -pub async fn convert_folder_meaning( - ctx: &Context, - folder_meaning: FolderMeaning, -) -> Result> { - let folder_config = match folder_meaning.to_config() { - Some(c) => c, - None => { - // Such folder cannot be configured, - // e.g. a `FolderMeaning::Spam` folder. - return Ok(None); - } - }; - - let folder = ctx - .get_config(folder_config) - .await - .with_context(|| format!("Failed to retrieve {folder_config} folder"))?; - - if let Some(watch_folder) = folder { - Ok(Some((folder_config, watch_folder))) - } else { - Ok(None) - } -} - async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result { if !ctx.get_config_bool(Config::FixIsChatmail).await? { ctx.set_config_internal( @@ -439,9 +404,10 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) .await?; } + let folder = imap.folder.clone(); // Update quota no more than once a minute. if ctx.quota_needs_update(session.transport_id(), 60).await - && let Err(err) = ctx.update_recent_quota(&mut session).await + && let Err(err) = ctx.update_recent_quota(&mut session, folder).await { warn!(ctx, "Failed to update quota: {:#}.", err); } @@ -479,7 +445,7 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) .await .context("Failed to register push token")?; - let session = fetch_idle(ctx, imap, session, FolderMeaning::Inbox).await?; + let session = fetch_idle(ctx, imap, session).await?; Ok(session) } @@ -488,32 +454,17 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) /// 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 { - let Some((folder_config, watch_folder)) = convert_folder_meaning(ctx, folder_meaning).await? - else { - // The folder is not configured. - // For example, this happens if the server does not have Sent folder - // but watching Sent folder is enabled. - connection.connectivity.set_not_configured(ctx); - connection.idle_interrupt_receiver.recv().await.ok(); - bail!("Cannot fetch folder {folder_meaning} because it is not configured"); - }; +async fn fetch_idle(ctx: &Context, connection: &mut Imap, mut session: Session) -> Result { + let watch_folder = connection.folder.clone(); - if folder_config == Config::ConfiguredInboxFolder { - session - .store_seen_flags_on_imap(ctx) - .await - .context("store_seen_flags_on_imap")?; - } + 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) + .fetch_move_delete(ctx, &mut session, &watch_folder) .await .context("fetch_move_delete")?; @@ -547,7 +498,7 @@ async fn fetch_idle( ctx, "IMAP session does not support IDLE, going to fake idle." ); - connection.fake_idle(ctx, watch_folder).await?; + connection.fake_idle(ctx, &watch_folder).await?; return Ok(session); } @@ -559,7 +510,7 @@ async fn fetch_idle( .unwrap_or_default() { info!(ctx, "IMAP IDLE is disabled, going to fake idle."); - connection.fake_idle(ctx, watch_folder).await?; + connection.fake_idle(ctx, &watch_folder).await?; return Ok(session); } @@ -579,73 +530,6 @@ async fn fetch_idle( Ok(session) } -/// Simplified IMAP loop to watch non-inbox folders. -async fn simple_imap_loop( - ctx: Context, - started: oneshot::Sender<()>, - inbox_handlers: ImapConnectionHandlers, - folder_meaning: FolderMeaning, -) { - use futures::future::FutureExt; - - info!(ctx, "Starting simple loop for {folder_meaning}."); - let ImapConnectionHandlers { - mut connection, - stop_token, - } = inbox_handlers; - - let ctx1 = ctx.clone(); - - let fut = async move { - let ctx = ctx1; - if let Err(()) = started.send(()) { - warn!( - ctx, - "Simple imap loop for {folder_meaning}, missing started receiver." - ); - return; - } - - let mut old_session: Option = None; - loop { - let session = if let Some(session) = old_session.take() { - session - } else { - info!(ctx, "Preparing new IMAP session for {folder_meaning}."); - match connection.prepare(&ctx).await { - Err(err) => { - warn!( - ctx, - "Failed to prepare {folder_meaning} 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) => { - info!( - ctx, - "IMAP loop iteration for {folder_meaning} finished, keeping the session" - ); - old_session = Some(session); - } - } - } - }; - - stop_token - .cancelled() - .map(|_| { - info!(ctx, "Shutting down IMAP loop for {folder_meaning}."); - }) - .race(fut) - .await; -} - async fn smtp_loop( ctx: Context, started: oneshot::Sender<()>, @@ -748,7 +632,6 @@ impl Scheduler { let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1); let mut inboxes = Vec::new(); - let mut oboxes = Vec::new(); let mut start_recvs = Vec::new(); for (transport_id, configured_login_param) in ConfiguredLoginParam::load_all(ctx).await? { @@ -760,30 +643,17 @@ impl Scheduler { task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers)) }; let addr = configured_login_param.addr.clone(); + let folder = configured_login_param + .imap_folder + .unwrap_or_else(|| "INBOX".to_string()); let inbox = SchedBox { addr: addr.clone(), - meaning: FolderMeaning::Inbox, + folder, conn_state, handle, }; inboxes.push(inbox); start_recvs.push(inbox_start_recv); - - if ctx.should_watch_mvbox().await? { - let (conn_state, handlers) = - ImapConnectionState::new(ctx, transport_id, configured_login_param).await?; - let (start_send, start_recv) = oneshot::channel(); - let ctx = ctx.clone(); - let meaning = FolderMeaning::Mvbox; - let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning)); - oboxes.push(SchedBox { - addr, - meaning, - conn_state, - handle, - }); - start_recvs.push(start_recv); - } } let smtp_handle = { @@ -810,7 +680,6 @@ impl Scheduler { let res = Self { inboxes, - oboxes, smtp, smtp_handle, ephemeral_handle, @@ -830,7 +699,7 @@ impl Scheduler { } fn boxes(&self) -> impl Iterator { - self.inboxes.iter().chain(self.oboxes.iter()) + self.inboxes.iter() } fn maybe_network(&self) { @@ -884,7 +753,7 @@ impl Scheduler { let timeout_duration = std::time::Duration::from_secs(30); let tracker = TaskTracker::new(); - for b in self.inboxes.into_iter().chain(self.oboxes.into_iter()) { + for b in self.inboxes { let context = context.clone(); tracker.spawn(async move { tokio::time::timeout(timeout_duration, b.handle) diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 78cd8f6c9a..706f85d20f 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -5,11 +5,10 @@ use std::{iter::once, ops::Deref, sync::Arc}; use anyhow::Result; use humansize::{BINARY, format_size}; +use crate::context::Context; use crate::events::EventType; -use crate::imap::{FolderMeaning, get_watched_folder_configs}; use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE}; use crate::stock_str; -use crate::{context::Context, log::LogExt}; use super::InnerSchedulerState; @@ -67,40 +66,33 @@ enum DetailedConnectivity { /// Connection is established and is idle. Idle, - - /// The folder was configured not to be watched or configured_*_folder is not set - NotConfigured, } impl DetailedConnectivity { - fn to_basic(&self) -> Option { + fn to_basic(&self) -> Connectivity { match self { - DetailedConnectivity::Error(_) => Some(Connectivity::NotConnected), - DetailedConnectivity::Uninitialized => Some(Connectivity::NotConnected), - DetailedConnectivity::Connecting => Some(Connectivity::Connecting), - DetailedConnectivity::Working => Some(Connectivity::Working), - DetailedConnectivity::InterruptingIdle => Some(Connectivity::Working), + DetailedConnectivity::Error(_) => Connectivity::NotConnected, + DetailedConnectivity::Uninitialized => Connectivity::NotConnected, + DetailedConnectivity::Connecting => Connectivity::Connecting, + DetailedConnectivity::Working => Connectivity::Working, + DetailedConnectivity::InterruptingIdle => Connectivity::Working, // At this point IMAP has just connected, // but does not know yet if there are messages to download. // We still convert this to Working state // so user can see "Updating..." and not "Connected" // which is reserved for idle state. - DetailedConnectivity::Preparing => Some(Connectivity::Working), - - // Just don't return a connectivity, probably the folder is configured not to be - // watched, so we are not interested in it. - DetailedConnectivity::NotConfigured => None, + DetailedConnectivity::Preparing => Connectivity::Working, - DetailedConnectivity::Idle => Some(Connectivity::Connected), + DetailedConnectivity::Idle => Connectivity::Connected, } } fn to_icon(&self) -> String { match self { - DetailedConnectivity::Error(_) - | DetailedConnectivity::Uninitialized - | DetailedConnectivity::NotConfigured => "".to_string(), + DetailedConnectivity::Error(_) | DetailedConnectivity::Uninitialized => { + "".to_string() + } DetailedConnectivity::Connecting => "".to_string(), DetailedConnectivity::Preparing | DetailedConnectivity::Working @@ -120,7 +112,6 @@ impl DetailedConnectivity { DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => { stock_str::connected(context).await } - DetailedConnectivity::NotConfigured => "Not configured".to_string(), } } @@ -139,7 +130,6 @@ impl DetailedConnectivity { DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Preparing | DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context).await, - DetailedConnectivity::NotConfigured => "Not configured".to_string(), } } @@ -151,7 +141,6 @@ impl DetailedConnectivity { DetailedConnectivity::Working => false, DetailedConnectivity::InterruptingIdle => false, DetailedConnectivity::Preparing => false, // Just connected, there may still be work to do. - DetailedConnectivity::NotConfigured => true, DetailedConnectivity::Idle => true, } } @@ -180,9 +169,6 @@ impl ConnectivityStore { pub(crate) fn set_preparing(&self, context: &Context) { self.set(context, DetailedConnectivity::Preparing); } - pub(crate) fn set_not_configured(&self, context: &Context) { - self.set(context, DetailedConnectivity::NotConfigured); - } pub(crate) fn set_idle(&self, context: &Context) { self.set(context, DetailedConnectivity::Idle); } @@ -190,7 +176,7 @@ impl ConnectivityStore { fn get_detailed(&self) -> DetailedConnectivity { self.0.lock().deref().clone() } - fn get_basic(&self) -> Option { + fn get_basic(&self) -> Connectivity { self.0.lock().to_basic() } fn get_all_work_done(&self) -> bool { @@ -201,27 +187,14 @@ impl ConnectivityStore { /// Set all folder states to InterruptingIdle in case they were `Idle` before. /// Called during `dc_maybe_network()` to make sure that `all_work_done()` /// returns false immediately after `dc_maybe_network()`. -pub(crate) fn idle_interrupted(inboxes: Vec, oboxes: Vec) { +pub(crate) fn idle_interrupted(inboxes: Vec) { for inbox in inboxes { let mut connectivity_lock = inbox.0.lock(); - // For the inbox, we also have to set the connectivity to InterruptingIdle if it was - // NotConfigured before: If all folders are NotConfigured, dc_get_connectivity() - // returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not - // return Connected until DC is completely done with fetching folders; this also - // includes scan_folders() which happens on the inbox thread. - if *connectivity_lock == DetailedConnectivity::Idle - || *connectivity_lock == DetailedConnectivity::NotConfigured - { - *connectivity_lock = DetailedConnectivity::InterruptingIdle; - } - } - - for state in oboxes { - let mut connectivity_lock = state.0.lock(); if *connectivity_lock == DetailedConnectivity::Idle { *connectivity_lock = DetailedConnectivity::InterruptingIdle; } } + // No need to send ConnectivityChanged, the user-facing connectivity doesn't change because // of what we do here. } @@ -234,9 +207,7 @@ pub(crate) fn maybe_network_lost(context: &Context, stores: Vec{incoming_messages}
    "); @@ -432,41 +401,14 @@ impl Context { let folders = folders_states .iter() .filter(|(folder_addr, ..)| *folder_addr == transport_addr); - for (_addr, folder, state) in folders { - let mut folder_added = false; - - if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) { - let f = self.get_config(config).await.log_err(self).ok().flatten(); - - if let Some(foldername) = f { - let detailed = &state.get_detailed(); - ret += &*detailed.to_icon(); - ret += " "; - if folder == &FolderMeaning::Inbox { - ret += &*domain_escaped; - } else { - ret += &*escaper::encode_minimal(&foldername); - } - ret += ": "; - ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await); - ret += "
    "; - - folder_added = true; - } - } - - if !folder_added && folder == &FolderMeaning::Inbox { - let detailed = &state.get_detailed(); - if let DetailedConnectivity::Error(_) = detailed { - // On the inbox thread, we also do some other things like scan_folders and run jobs - // so, maybe, the inbox is not watched, but something else went wrong - - ret += &*detailed.to_icon(); - ret += " "; - ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await); - ret += "
    "; - } - } + for (_addr, _folder, state) in folders { + let detailed = &state.get_detailed(); + ret += &*detailed.to_icon(); + ret += " "; + ret += &*domain_escaped; + ret += ": "; + ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await); + ret += "
    "; } let Some(quota) = quota.get(&transport_id) else { diff --git a/src/sql.rs b/src/sql.rs index bd610e511f..f0405c0758 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -9,7 +9,6 @@ use rusqlite::{Connection, OpenFlags, Row, config::DbConfig, types::ValueRef}; use tokio::sync::RwLock; use crate::blob::BlobObject; -use crate::chat::add_device_msg; use crate::config::Config; use crate::constants::DC_CHAT_ID_TRASH; use crate::context::Context; @@ -18,13 +17,11 @@ use crate::ephemeral::start_ephemeral_timers; use crate::imex::BLOBS_BACKUP_NAME; use crate::location::delete_orphaned_poi_locations; use crate::log::{LogExt, warn}; -use crate::message::Message; use crate::message::MsgId; use crate::net::dns::prune_dns_cache; use crate::net::http::http_cache_cleanup; use crate::net::prune_connection_history; use crate::param::{Param, Params}; -use crate::stock_str; use crate::tools::{SystemTime, Time, delete_file, time, time_elapsed}; /// Extension to [`rusqlite::ToSql`] trait @@ -868,12 +865,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> { ); } - maybe_add_mvbox_move_deprecation_message(context) - .await - .context("maybe_add_mvbox_move_deprecation_message") - .log_err(context) - .ok(); - if let Err(err) = incremental_vacuum(context).await { warn!(context, "Failed to run incremental vacuum: {err:#}."); } @@ -933,18 +924,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> { Ok(()) } -/// Adds device message about `mvbox_move` config deprecation -/// if the user has it enabled. -async fn maybe_add_mvbox_move_deprecation_message(context: &Context) -> Result<()> { - if !context.get_config_bool(Config::OnlyFetchMvbox).await? - && context.get_config_bool(Config::MvboxMove).await? - { - let mut msg = Message::new_text(stock_str::mvbox_move_deprecation(context).await); - add_device_msg(context, Some("mvbox_move_deprecation"), Some(&mut msg)).await?; - } - Ok(()) -} - /// Get the value of a column `idx` of the `row` as `Vec`. pub fn row_get_vec(row: &Row, idx: usize) -> rusqlite::Result> { row.get(idx).or_else(|err| match row.get_ref(idx)? { diff --git a/src/stock_str.rs b/src/stock_str.rs index b2830955ea..394653c52a 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -407,11 +407,6 @@ https://delta.chat/donate"))] #[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))] ChatUnencryptedExplanation = 230, - #[strum(props( - fallback = "You are using the legacy option \"Settings → Advanced → Move automatically to DeltaChat Folder\".\n\nThis option will be removed in a few weeks and you should disable it already today.\n\nIf having chat messages mixed into your inbox is a problem, see https://delta.chat/legacy-move" - ))] - MvboxMoveDeprecation = 231, - #[strum(props(fallback = "Outgoing audio call"))] OutgoingAudioCall = 232, @@ -1269,11 +1264,6 @@ pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String { translated(context, StockMessage::ChatUnencryptedExplanation).await } -/// Stock string: `You are using the legacy option "Move automatically to DeltaChat Folder`… -pub(crate) async fn mvbox_move_deprecation(context: &Context) -> String { - translated(context, StockMessage::MvboxMoveDeprecation).await -} - impl Viewtype { /// returns Localized name for message viewtype pub async fn to_locale_string(&self, context: &Context) -> String { diff --git a/src/test_utils.rs b/src/test_utils.rs index bbef12acc4..44cbef1017 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -567,7 +567,6 @@ impl TestContext { .unwrap(); ctx.set_config(Config::BccSelf, Some("1")).await.unwrap(); ctx.set_config(Config::SyncMsgs, Some("0")).await.unwrap(); - ctx.set_config(Config::MvboxMove, Some("0")).await.unwrap(); Self { ctx, diff --git a/src/transport.rs b/src/transport.rs index 6343968abd..092ca5ead1 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -19,6 +19,7 @@ use crate::config::Config; use crate::configure::server_params::{ServerParams, expand_param_vector}; use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2}; use crate::context::Context; +use crate::ensure_and_debug_assert; use crate::events::EventType; use crate::login_param::EnteredLoginParam; use crate::net::load_connection_timestamp; @@ -163,22 +164,30 @@ pub(crate) struct ConfiguredLoginParam { /// `From:` address that was used at the time of configuration. pub addr: String, + /// List of IMAP candidates to try. pub imap: Vec, - // Custom IMAP user. - // - // This overwrites autoconfig from the provider database - // if non-empty. + /// Custom IMAP user. + /// + /// This overwrites autoconfig from the provider database + /// if non-empty. pub imap_user: String, pub imap_password: String, + // IMAP folder to watch. + // + // If not stored, should be interpreted as "INBOX". + // If stored, should be a folder name and not empty. + pub imap_folder: Option, + + /// List of SMTP candidates to try. pub smtp: Vec, - // Custom SMTP user. - // - // This overwrites autoconfig from the provider database - // if non-empty. + /// Custom SMTP user. + /// + /// This overwrites autoconfig from the provider database + /// if non-empty. pub smtp_user: String, pub smtp_password: String, @@ -199,6 +208,13 @@ pub(crate) struct ConfiguredLoginParam { pub(crate) struct ConfiguredLoginParamJson { pub addr: String, pub imap: Vec, + + /// IMAP folder to watch. + /// + /// Defaults to "INBOX" if unset. + #[serde(skip_serializing_if = "Option::is_none")] + pub imap_folder: Option, + pub imap_user: String, pub imap_password: String, pub smtp: Vec, @@ -545,6 +561,7 @@ impl ConfiguredLoginParam { Ok(Some(ConfiguredLoginParam { addr, imap, + imap_folder: None, imap_user: mail_user, imap_password: mail_pw, smtp, @@ -569,11 +586,18 @@ impl ConfiguredLoginParam { pub(crate) fn from_json(json: &str) -> Result { let json: ConfiguredLoginParamJson = serde_json::from_str(json)?; + ensure_and_debug_assert!( + json.imap_folder + .as_ref() + .is_none_or(|folder| !folder.is_empty()), + "Configured watched folder name cannot be empty" + ); let provider = json.provider_id.and_then(|id| get_provider_by_id(&id)); Ok(ConfiguredLoginParam { addr: json.addr, imap: json.imap, + imap_folder: json.imap_folder, imap_user: json.imap_user, imap_password: json.imap_password, smtp: json.smtp, @@ -611,6 +635,7 @@ impl From for ConfiguredLoginParamJson { imap: configured_login_param.imap, imap_user: configured_login_param.imap_user, imap_password: configured_login_param.imap_password, + imap_folder: configured_login_param.imap_folder, smtp: configured_login_param.smtp, smtp_user: configured_login_param.smtp_user, smtp_password: configured_login_param.smtp_password, @@ -629,9 +654,16 @@ pub(crate) async fn save_transport( configured: &ConfiguredLoginParamJson, add_timestamp: i64, ) -> Result { + ensure_and_debug_assert!( + configured + .imap_folder + .as_ref() + .is_none_or(|folder| !folder.is_empty()), + "Configured watched folder name cannot be empty" + ); + let addr = addr_normalize(&configured.addr); let configured_addr = context.get_config(Config::ConfiguredAddr).await?; - let mut modified = context .sql .execute( @@ -820,6 +852,7 @@ mod tests { }, user: "alice".to_string(), }], + imap_folder: None, imap_user: "".to_string(), imap_password: "foo".to_string(), smtp: vec![ConfiguredServerLoginParam { @@ -928,6 +961,7 @@ mod tests { user: user.to_string(), }, ], + imap_folder: None, imap_user: "alice@posteo.de".to_string(), imap_password: "foobarbaz".to_string(), smtp: vec![ @@ -1041,6 +1075,7 @@ mod tests { }, user: addr.clone(), }], + imap_folder: None, imap_user: addr.clone(), imap_password: "foobarbaz".to_string(), smtp: vec![ConfiguredServerLoginParam {