feat: remove MvboxMove and OnlyFetchMvbox

This commit is contained in:
link2xt
2026-01-26 23:11:16 +00:00
parent d7b3a85127
commit 1b42e74b52
25 changed files with 329 additions and 883 deletions

View File

@@ -430,14 +430,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* 1=send a copy of outgoing messages to self (default). * 1=send a copy of outgoing messages to self (default).
* Sending messages to self is needed for a proper multi-account setup, * 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. * 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_emails` = DC_SHOW_EMAILS_OFF (0)=
* show direct replies to chats only, * show direct replies to chats only,
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)= * DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=

View File

@@ -23,6 +23,12 @@ pub struct EnteredLoginParam {
/// Imap server port. /// Imap server port.
pub imap_port: Option<u16>, pub imap_port: Option<u16>,
/// IMAP server folder.
///
/// Defaults to "INBOX" if not set.
/// Should not be an empty string.
pub imap_folder: Option<String>,
/// Imap socket security. /// Imap socket security.
pub imap_security: Option<Socket>, pub imap_security: Option<Socket>,
@@ -66,6 +72,7 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
password: param.imap.password, password: param.imap.password,
imap_server: param.imap.server.into_option(), imap_server: param.imap.server.into_option(),
imap_port: param.imap.port.into_option(), imap_port: param.imap.port.into_option(),
imap_folder: param.imap.folder.into_option(),
imap_security: imap_security.into_option(), imap_security: imap_security.into_option(),
imap_user: param.imap.user.into_option(), imap_user: param.imap.user.into_option(),
smtp_server: param.smtp.server.into_option(), smtp_server: param.smtp.server.into_option(),
@@ -85,14 +92,15 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
fn try_from(param: EnteredLoginParam) -> Result<Self> { fn try_from(param: EnteredLoginParam) -> Result<Self> {
Ok(Self { Ok(Self {
addr: param.addr, addr: param.addr,
imap: dc::EnteredServerLoginParam { imap: dc::EnteredImapLoginParam {
server: param.imap_server.unwrap_or_default(), server: param.imap_server.unwrap_or_default(),
port: param.imap_port.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(), security: param.imap_security.unwrap_or_default().into(),
user: param.imap_user.unwrap_or_default(), user: param.imap_user.unwrap_or_default(),
password: param.password, password: param.password,
}, },
smtp: dc::EnteredServerLoginParam { smtp: dc::EnteredSmtpLoginParam {
server: param.smtp_server.unwrap_or_default(), server: param.smtp_server.unwrap_or_default(),
port: param.smtp_port.unwrap_or_default(), port: param.smtp_port.unwrap_or_default(),
security: param.smtp_security.unwrap_or_default().into(), security: param.smtp_security.unwrap_or_default().into(),

View File

@@ -2,32 +2,13 @@ import logging
import re import re
import time import time
import pytest
from imap_tools import AND, U from imap_tools import AND, U
from deltachat_rpc_client import Contact, EventType, Message 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): 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 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 processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped. 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() addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account() ac2 = acfactory.get_unconfigured_account()
ac2.add_or_update_transport({"addr": addr, "password": password}) 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() assert ac2.is_configured()
ac2.bring_online() ac2.bring_online()
@@ -55,11 +33,17 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
react_str = "\N{THUMBS UP SIGN}" react_str = "\N{THUMBS UP SIGN}"
msg1.send_reaction(react_str).wait_until_delivered() 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 = direct_imap(ac2)
ac2_direct_imap.create_folder("Movebox")
ac2_direct_imap.connect() ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True): 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") logging.info("receiving messages by ac2")
ac2.start_io() ac2.start_io()
@@ -72,33 +56,22 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
assert list(reactions.reactions_by_contact.values())[0] == [react_str] assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_move_works_on_self_sent(acfactory, direct_imap): def test_moved_markseen(acfactory, direct_imap, log):
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.""" """Test that message already moved to DeltaChat folder is marked as seen."""
ac1, ac2 = acfactory.get_online_accounts(2) ac1 = acfactory.get_online_account()
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2.add_or_update_transport({"addr": addr, "password": password})
ac2.bring_online()
log.section("ac2: creating DeltaChat folder")
ac2_direct_imap = direct_imap(ac2) ac2_direct_imap = direct_imap(ac2)
ac2_direct_imap.create_folder("DeltaChat") ac2_direct_imap.create_folder("DeltaChat")
ac2.set_config("mvbox_move", "1")
ac2.set_config("delete_server_after", "0") ac2.set_config("delete_server_after", "0")
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request. ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"})
ac2.bring_online() ac2.bring_online()
ac2.stop_io() ac2.stop_io()
@@ -108,6 +81,7 @@ def test_moved_markseen(acfactory, direct_imap):
idle2.wait_for_new_message() idle2.wait_for_new_message()
# Emulate moving of the message to DeltaChat folder by Sieve rule. # Emulate moving of the message to DeltaChat folder by Sieve rule.
log.section("ac2: moving message into DeltaChat folder")
ac2_direct_imap.conn.move(["*"], "DeltaChat") ac2_direct_imap.conn.move(["*"], "DeltaChat")
ac2_direct_imap.select_folder("DeltaChat") ac2_direct_imap.select_folder("DeltaChat")
assert len(list(ac2_direct_imap.conn.fetch("*", mark_seen=False))) == 1 assert len(list(ac2_direct_imap.conn.fetch("*", mark_seen=False))) == 1
@@ -131,17 +105,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 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):
def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
ac1, ac2 = acfactory.get_online_accounts(2) ac1, ac2 = acfactory.get_online_accounts(2)
for ac in ac1, ac2: for ac in ac1, ac2:
ac.set_config("delete_server_after", "0") 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. # Do not send BCC to self, we only want to test MDN on ac1.
ac1.set_config("bcc_self", "0") ac1.set_config("bcc_self", "0")
@@ -150,10 +118,7 @@ def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
msg = ac2.wait_for_incoming_msg() msg = ac2.wait_for_incoming_msg()
msg.mark_seen() msg.mark_seen()
if mvbox_move: rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
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.")
for ac in ac1, ac2: for ac in ac1, ac2:
while True: while True:
@@ -161,12 +126,11 @@ def test_markseen_message_and_mdn(acfactory, direct_imap, mvbox_move):
if event.kind == EventType.INFO and rex.search(event.msg): if event.kind == EventType.INFO and rex.search(event.msg):
break break
folder = "mvbox" if mvbox_move else "inbox"
ac1_direct_imap = direct_imap(ac1) ac1_direct_imap = direct_imap(ac1)
ac2_direct_imap = direct_imap(ac2) ac2_direct_imap = direct_imap(ac2)
ac1_direct_imap.select_config_folder(folder) ac1_direct_imap.select_folder("INBOX")
ac2_direct_imap.select_config_folder(folder) ac2_direct_imap.select_folder("INBOX")
# Check that the mdn is marked as seen # Check that the mdn is marked as seen
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1 assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1

View File

@@ -9,10 +9,6 @@ def test_add_second_address(acfactory) -> None:
account = acfactory.new_configured_account() account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1 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" assert account.get_config("show_emails") == "2"
qr = acfactory.get_account_qr() qr = acfactory.get_account_qr()
@@ -32,32 +28,10 @@ def test_add_second_address(acfactory) -> None:
account.delete_transport(second_addr) account.delete_transport(second_addr)
assert len(account.list_transports()) == 2 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 # show_emails does not matter for multi-relay, can be set to anything
account.set_config("show_emails", "0") 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: def test_second_transport_without_classic_emails(acfactory) -> None:
"""Test that second transport can be configured if classic emails are not fetched.""" """Test that second transport can be configured if classic emails are not fetched."""
account = acfactory.new_configured_account() account = acfactory.new_configured_account()
@@ -147,44 +121,13 @@ def test_download_on_demand(acfactory) -> None:
assert msg.get_snapshot().download_state == dstate 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: def test_reconfigure_transport(acfactory) -> None:
"""Test that reconfiguring the transport works """Test that reconfiguring the transport works."""
even if settings not supported for multi-transport
like mvbox_move are enabled."""
account = acfactory.get_online_account() account = acfactory.get_online_account()
account.set_config("mvbox_move", "1")
[transport] = account.list_transports() [transport] = account.list_transports()
account.add_or_update_transport(transport) 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: def test_transport_synchronization(acfactory, log) -> None:
"""Test synchronization of transports between devices.""" """Test synchronization of transports between devices."""

View File

@@ -522,7 +522,6 @@ class ACFactory:
ac = self.get_unconfigured_account() ac = self.get_unconfigured_account()
assert "addr" in configdict and "mail_pw" in configdict, configdict assert "addr" in configdict and "mail_pw" in configdict, configdict
configdict.setdefault("bcc_self", False) configdict.setdefault("bcc_self", False)
configdict.setdefault("mvbox_move", False)
configdict.setdefault("sync_msgs", False) configdict.setdefault("sync_msgs", False)
configdict.setdefault("delete_server_after", 0) configdict.setdefault("delete_server_after", 0)
ac.update_config(configdict) ac.update_config(configdict)

View File

@@ -52,19 +52,19 @@ class TestOfflineAccountBasic:
def test_set_config_int_conversion(self, acfactory): def test_set_config_int_conversion(self, acfactory):
ac1 = acfactory.get_unconfigured_account() ac1 = acfactory.get_unconfigured_account()
ac1.set_config("mvbox_move", False) ac1.set_config("bcc_self", False)
assert ac1.get_config("mvbox_move") == "0" assert ac1.get_config("bcc_self") == "0"
ac1.set_config("mvbox_move", True) ac1.set_config("bcc_self", True)
assert ac1.get_config("mvbox_move") == "1" assert ac1.get_config("bcc_self") == "1"
ac1.set_config("mvbox_move", 0) ac1.set_config("bcc_self", 0)
assert ac1.get_config("mvbox_move") == "0" assert ac1.get_config("bcc_self") == "0"
ac1.set_config("mvbox_move", 1) ac1.set_config("bcc_self", 1)
assert ac1.get_config("mvbox_move") == "1" assert ac1.get_config("bcc_self") == "1"
def test_update_config(self, acfactory): def test_update_config(self, acfactory):
ac1 = acfactory.get_unconfigured_account() ac1 = acfactory.get_unconfigured_account()
ac1.update_config({"mvbox_move": False}) ac1.update_config({"bcc_self": True})
assert ac1.get_config("mvbox_move") == "0" assert ac1.get_config("bcc_self") == "1"
def test_has_bccself(self, acfactory): def test_has_bccself(self, acfactory):
ac1 = acfactory.get_unconfigured_account() ac1 = acfactory.get_unconfigured_account()

View File

@@ -155,18 +155,6 @@ pub enum Config {
#[strum(props(default = "1"))] #[strum(props(default = "1"))]
MdnsEnabled, 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. /// Whether to show classic emails or only chat messages.
#[strum(props(default = "2"))] // also change ShowEmails.default() on changes #[strum(props(default = "2"))] // also change ShowEmails.default() on changes
ShowEmails, ShowEmails,
@@ -268,9 +256,6 @@ pub enum Config {
/// Configured folder for incoming messages. /// Configured folder for incoming messages.
ConfiguredInboxFolder, ConfiguredInboxFolder,
/// Configured folder for chat messages.
ConfiguredMvboxFolder,
/// Unix timestamp of the last successful configuration. /// Unix timestamp of the last successful configuration.
ConfiguredTimestamp, ConfiguredTimestamp,
@@ -467,7 +452,6 @@ impl Config {
self, self,
Self::Displayname Self::Displayname
| Self::MdnsEnabled | Self::MdnsEnabled
| Self::MvboxMove
| Self::ShowEmails | Self::ShowEmails
| Self::Selfavatar | Self::Selfavatar
| Self::Selfstatus, | Self::Selfstatus,
@@ -476,10 +460,7 @@ impl Config {
/// Whether the config option needs an IO scheduler restart to take effect. /// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool { pub(crate) fn needs_io_restart(&self) -> bool {
matches!( matches!(self, Config::ConfiguredAddr)
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr
)
} }
} }
@@ -594,12 +575,6 @@ impl Context {
.is_some_and(|x| x != 0)) .is_some_and(|x| x != 0))
} }
/// Returns true if movebox ("DeltaChat" folder) should be watched.
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
}
/// Returns true if sync messages should be sent. /// Returns true if sync messages should be sent.
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> { pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SyncMsgs).await? Ok(self.get_config_bool(Config::SyncMsgs).await?
@@ -681,8 +656,6 @@ impl Context {
| Config::ProxyEnabled | Config::ProxyEnabled
| Config::BccSelf | Config::BccSelf
| Config::MdnsEnabled | Config::MdnsEnabled
| Config::MvboxMove
| Config::OnlyFetchMvbox
| Config::Configured | Config::Configured
| Config::Bot | Config::Bot
| Config::NotifyAboutWrongPw | Config::NotifyAboutWrongPw
@@ -705,11 +678,6 @@ impl Context {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> { pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
Self::check_config(key, value)?; 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() { let _pause = match key.needs_io_restart() {
true => self.scheduler.pause(self).await?, true => self.scheduler.pause(self).await?,
_ => Default::default(), _ => Default::default(),
@@ -788,12 +756,6 @@ impl Context {
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref()) .set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
.await?; .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 => { Config::ConfiguredAddr => {
let Some(addr) = value else { let Some(addr) = value else {
bail!("Cannot unset configured_addr"); bail!("Cannot unset configured_addr");

View File

@@ -196,11 +196,11 @@ async fn test_sync() -> Result<()> {
sync(&alice0, &alice1).await; sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false); 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?; let val = alice0.get_config_bool(Config::ShowEmails).await?;
alice0.set_config_bool(key, !val).await?; alice0.set_config_bool(Config::ShowEmails, !val).await?;
sync(&alice0, &alice1).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. // `Config::SyncMsgs` mustn't be synced.

View File

@@ -273,31 +273,16 @@ impl Context {
(&param.addr,), (&param.addr,),
) )
.await? .await?
{ && self
// 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
.sql .sql
.count("SELECT COUNT(*) FROM transports", ()) .count("SELECT COUNT(*) FROM transports", ())
.await? .await?
>= MAX_TRANSPORT_RELAYS >= MAX_TRANSPORT_RELAYS
{ {
bail!( bail!(
"You have reached the maximum number of relays ({}).", "You have reached the maximum number of relays ({}).",
MAX_TRANSPORT_RELAYS MAX_TRANSPORT_RELAYS
) )
}
} }
let provider = match configure(self, param).await { let provider = match configure(self, param).await {
@@ -510,6 +495,7 @@ async fn get_configured_param(
.collect(), .collect(),
imap_user: param.imap.user.clone(), imap_user: param.imap.user.clone(),
imap_password: param.imap.password.clone(), imap_password: param.imap.password.clone(),
imap_folder: Some(param.imap.folder.clone()).filter(|folder| !folder.is_empty()),
smtp: servers smtp: servers
.iter() .iter()
.filter_map(|params| { .filter_map(|params| {
@@ -605,10 +591,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
progress!(ctx, 900); progress!(ctx, 900);
let is_configured = ctx.is_configured().await?; let is_configured = ctx.is_configured().await?;
if !is_configured {
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
}
if !ctx.get_config_bool(Config::FixIsChatmail).await? { if !ctx.get_config_bool(Config::FixIsChatmail).await? {
if imap_session.is_chatmail() { if imap_session.is_chatmail() {
ctx.sql.set_raw_config("is_chatmail", Some("1")).await?; ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
@@ -772,7 +754,7 @@ pub enum Error {
mod tests { mod tests {
use super::*; use super::*;
use crate::config::Config; use crate::config::Config;
use crate::login_param::EnteredServerLoginParam; use crate::login_param::EnteredImapLoginParam;
use crate::test_utils::TestContext; use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -791,7 +773,7 @@ mod tests {
let entered_param = EnteredLoginParam { let entered_param = EnteredLoginParam {
addr: "alice@example.org".to_string(), addr: "alice@example.org".to_string(),
imap: EnteredServerLoginParam { imap: EnteredImapLoginParam {
user: "alice@example.net".to_string(), user: "alice@example.net".to_string(),
password: "foobar".to_string(), password: "foobar".to_string(),
..Default::default() ..Default::default()

View File

@@ -210,11 +210,6 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
/// usage by UIs. /// usage by UIs.
pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000; pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
// Key for the folder configuration version (see below).
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
// 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 = 5;
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into // If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting // chunks. This does not affect MIME's `To:` header. Can be overwritten by setting
// `max_smtp_rcpt_to` in the provider db. // `max_smtp_rcpt_to` in the provider db.

View File

@@ -20,7 +20,7 @@ use crate::constants::{self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_VERSI
use crate::contact::{Contact, ContactId}; use crate::contact::{Contact, ContactId};
use crate::debug_logging::DebugLogging; use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events}; use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata}; use crate::imap::{Imap, ServerMetadata};
use crate::key::self_fingerprint; use crate::key::self_fingerprint;
use crate::log::warn; use crate::log::warn;
use crate::logged_debug_assert; use crate::logged_debug_assert;
@@ -29,7 +29,7 @@ use crate::net::tls::TlsSessionStore;
use crate::peer_channels::Iroh; use crate::peer_channels::Iroh;
use crate::push::PushSubscriber; use crate::push::PushSubscriber;
use crate::quota::QuotaInfo; use crate::quota::QuotaInfo;
use crate::scheduler::{ConnectivityStore, SchedulerState, convert_folder_meaning}; use crate::scheduler::{ConnectivityStore, SchedulerState};
use crate::sql::Sql; use crate::sql::Sql;
use crate::stock_str::StockStrings; use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp; use crate::timesmearing::SmearedTimestamp;
@@ -623,17 +623,10 @@ impl Context {
let mut session = connection.prepare(self).await?; let mut session = connection.prepare(self).await?;
// Fetch IMAP folders. // Fetch IMAP folders.
// Inbox is fetched before Mvbox because fetching from Inbox let folder = connection.folder.clone();
// may result in moving some messages to Mvbox. connection
for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] { .fetch_move_delete(self, &mut session, &folder)
if let Some((_folder_config, watch_folder)) = .await?;
convert_folder_meaning(self, folder_meaning).await?
{
connection
.fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
.await?;
}
}
// Update quota (to send warning if full) - but only check it once in a while. // Update quota (to send warning if full) - but only check it once in a while.
// note: For now this only checks quota of primary transport, // note: For now this only checks quota of primary transport,
@@ -644,7 +637,7 @@ impl Context {
DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT,
) )
.await .await
&& let Err(err) = self.update_recent_quota(&mut session).await && let Err(err) = self.update_recent_quota(&mut session, folder).await
{ {
warn!(self, "Failed to update quota: {err:#}."); warn!(self, "Failed to update quota: {err:#}.");
} }
@@ -884,23 +877,6 @@ impl Context {
Err(err) => format!("<key failure: {err}>"), Err(err) => format!("<key failure: {err}>"),
}; };
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(|| "<unset>".to_string());
let configured_mvbox_folder = self
.get_config(Config::ConfiguredMvboxFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info(); let mut res = get_info();
// insert values // insert values
@@ -976,14 +952,6 @@ impl Context {
.await? .await?
.to_string(), .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("mdns_enabled", mdns_enabled.to_string());
res.insert("bcc_self", bcc_self.to_string()); res.insert("bcc_self", bcc_self.to_string());
res.insert("sync_msgs", sync_msgs.to_string()); res.insert("sync_msgs", sync_msgs.to_string());
@@ -1283,12 +1251,6 @@ ORDER BY m.timestamp DESC,m.id DESC",
Ok(list) 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<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
Ok(mvbox.as_deref() == Some(folder_name))
}
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

@@ -27,13 +27,14 @@ use crate::calls::{
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg}; use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
use crate::chatlist_events; use crate::chatlist_events;
use crate::config::Config; use crate::config::Config;
use crate::constants::{self, Blocked, DC_VERSION_STR}; use crate::constants::{Blocked, DC_VERSION_STR};
use crate::contact::ContactId; use crate::contact::ContactId;
use crate::context::Context; use crate::context::Context;
use crate::ensure_and_debug_assert;
use crate::events::EventType; use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::log::{LogExt, warn}; use crate::log::{LogExt, warn};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId}; use crate::message::{self, Message, MessageState, MsgId};
use crate::mimeparser; use crate::mimeparser;
use crate::net::proxy::ProxyConfig; use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream; use crate::net::session::SessionStream;
@@ -91,6 +92,9 @@ pub(crate) struct Imap {
oauth2: bool, oauth2: bool,
/// Watched folder.
pub(crate) folder: String,
authentication_failed_once: bool, authentication_failed_once: bool,
pub(crate) connectivity: ConnectivityStore, pub(crate) connectivity: ConnectivityStore,
@@ -162,7 +166,6 @@ pub enum FolderMeaning {
/// Spam folder. /// Spam folder.
Spam, Spam,
Inbox, Inbox,
Mvbox,
Trash, Trash,
/// Virtual folders. /// Virtual folders.
@@ -174,19 +177,6 @@ pub enum FolderMeaning {
Virtual, Virtual,
} }
impl FolderMeaning {
pub fn to_config(self) -> Option<Config> {
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<T: Iterator<Item = (i64, u32, String)>> { struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
inner: Peekable<T>, inner: Peekable<T>,
} }
@@ -263,6 +253,11 @@ impl Imap {
let addr = &param.addr; let addr = &param.addr;
let strict_tls = param.strict_tls(proxy_config.is_some()); let strict_tls = param.strict_tls(proxy_config.is_some());
let oauth2 = param.oauth2; 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); let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
Ok(Imap { Ok(Imap {
transport_id, transport_id,
@@ -273,6 +268,7 @@ impl Imap {
proxy_config, proxy_config,
strict_tls, strict_tls,
oauth2, oauth2,
folder,
authentication_failed_once: false, authentication_failed_once: false,
connectivity: Default::default(), connectivity: Default::default(),
conn_last_try: UNIX_EPOCH, conn_last_try: UNIX_EPOCH,
@@ -485,7 +481,7 @@ impl Imap {
/// that folders are created and IMAP capabilities are determined. /// that folders are created and IMAP capabilities are determined.
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> { pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
let configuring = false; let configuring = false;
let mut session = match self.connect(context, configuring).await { let session = match self.connect(context, configuring).await {
Ok(session) => session, Ok(session) => session,
Err(err) => { Err(err) => {
self.connectivity.set_err(context, &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) Ok(session)
} }
@@ -513,15 +501,15 @@ impl Imap {
context: &Context, context: &Context,
session: &mut Session, session: &mut Session,
watch_folder: &str, watch_folder: &str,
folder_meaning: FolderMeaning,
) -> Result<()> { ) -> Result<()> {
ensure_and_debug_assert!(!watch_folder.is_empty(), "Watched folder cannot be empty");
if !context.sql.is_open().await { if !context.sql.is_open().await {
// probably shutdown // probably shutdown
bail!("IMAP operation attempted while it is torn down"); bail!("IMAP operation attempted while it is torn down");
} }
let msgs_fetched = self let msgs_fetched = self
.fetch_new_messages(context, session, watch_folder, folder_meaning) .fetch_new_messages(context, session, watch_folder)
.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() {
@@ -549,14 +537,7 @@ impl Imap {
context: &Context, context: &Context,
session: &mut Session, session: &mut Session,
folder: &str, folder: &str,
folder_meaning: FolderMeaning,
) -> Result<bool> { ) -> Result<bool> {
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 let folder_exists = session
.select_with_uidvalidity(context, folder) .select_with_uidvalidity(context, folder)
.await .await
@@ -573,9 +554,7 @@ impl Imap {
let mut read_cnt = 0; let mut read_cnt = 0;
loop { loop {
let (n, fetch_more) = self let (n, fetch_more) = self.fetch_new_msg_batch(context, session, folder).await?;
.fetch_new_msg_batch(context, session, folder, folder_meaning)
.await?;
read_cnt += n; read_cnt += n;
if !fetch_more { if !fetch_more {
return Ok(read_cnt > 0); return Ok(read_cnt > 0);
@@ -590,7 +569,6 @@ impl Imap {
context: &Context, context: &Context,
session: &mut Session, session: &mut Session,
folder: &str, folder: &str,
folder_meaning: FolderMeaning,
) -> Result<(usize, bool)> { ) -> Result<(usize, bool)> {
let transport_id = self.transport_id; let transport_id = self.transport_id;
let uid_validity = get_uidvalidity(context, transport_id, folder).await?; let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
@@ -660,13 +638,7 @@ impl Imap {
info!(context, "Deleting locally deleted message {message_id}."); info!(context, "Deleting locally deleted message {message_id}.");
} }
let _target; let target = if delete { "" } else { folder };
let target = if delete {
""
} else {
_target = target_folder(context, folder, folder_meaning, &headers).await?;
&_target
};
context context
.sql .sql
@@ -694,18 +666,9 @@ impl Imap {
// message, move it to the movebox and then download the second message before // 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. // downloading the first one, if downloading from inbox before moving is allowed.
if folder == target if folder == target
// Never download messages directly from the spam folder. && prefetch_should_download(context, &headers, &message_id, fetch_response.flags())
// If the sender is known, the message will be moved to the Inbox or Mvbox .await
// and then we download the message from there. .context("prefetch_should_download")?
// 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")?
{ {
if headers if headers
.get_header_value(HeaderDef::ChatIsPostMessage) .get_header_value(HeaderDef::ChatIsPostMessage)
@@ -1621,13 +1584,8 @@ impl Session {
// Store new encrypted device token on the server // Store new encrypted device token on the server
// even if it is the same as the old one. // even if it is the same as the old one.
if let Some(encrypted_device_token) = new_encrypted_device_token { 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( self.run_command_and_check_ok(&format_setmetadata(
&folder, "INBOX",
&encrypted_device_token, &encrypted_device_token,
)) ))
.await .await
@@ -1672,117 +1630,6 @@ impl Session {
} }
Ok(()) 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
/// <https://datatracker.ietf.org/doc/html/rfc4314>).
///
/// Returns first found folder name.
async fn configure_mvbox<'a>(
&mut self,
context: &Context,
folders: &[&'a str],
) -> Result<Option<&'a str>> {
// 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 { impl Session {
@@ -1916,15 +1763,7 @@ async fn spam_target_folder_cfg(
return Ok(None); return Ok(None);
} }
if needs_move_to_mvbox(context, headers).await? Ok(Some(Config::ConfiguredInboxFolder))
// 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))
}
} }
/// Returns `ConfiguredInboxFolder` or `ConfiguredMvboxFolder` if /// Returns `ConfiguredInboxFolder` or `ConfiguredMvboxFolder` if
@@ -1935,16 +1774,12 @@ pub async fn target_folder_cfg(
folder_meaning: FolderMeaning, folder_meaning: FolderMeaning,
headers: &[mailparse::MailHeader<'_>], headers: &[mailparse::MailHeader<'_>],
) -> Result<Option<Config>> { ) -> Result<Option<Config>> {
if context.is_mvbox(folder).await? { if folder == "DeltaChat" {
return Ok(None); return Ok(None);
} }
if folder_meaning == FolderMeaning::Spam { if folder_meaning == FolderMeaning::Spam {
spam_target_folder_cfg(context, headers).await 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 { } else {
Ok(None) Ok(None)
} }
@@ -1965,36 +1800,6 @@ pub async fn target_folder(
} }
} }
async fn needs_move_to_mvbox(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
) -> Result<bool> {
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. /// 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? // TODO: lots languages missing - maybe there is a list somewhere on other MUAs?
// however, if we fail to find out the sent-folder, // however, if we fail to find out the sent-folder,
@@ -2325,21 +2130,6 @@ async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Resul
.unwrap_or(0)) .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<bool> {
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 /// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5> /// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars) /// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
@@ -2399,23 +2189,5 @@ impl std::fmt::Display for UidRange {
} }
} }
pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
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<Vec<String>> {
let mut res = Vec::new();
for folder_config in get_watched_folder_configs(context).await? {
if let Some(folder) = context.get_config(folder_config).await? {
res.push(folder);
}
}
Ok(res)
}
#[cfg(test)] #[cfg(test)]
mod imap_tests; mod imap_tests;

View File

@@ -115,11 +115,7 @@ impl Session {
impl Imap { impl Imap {
/// Idle using polling. /// Idle using polling.
pub(crate) async fn fake_idle( pub(crate) async fn fake_idle(&mut self, context: &Context, watch_folder: &str) -> Result<()> {
&mut self,
context: &Context,
watch_folder: String,
) -> Result<()> {
let fake_idle_start_time = tools::Time::now(); let fake_idle_start_time = tools::Time::now();
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder); info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);

View File

@@ -100,7 +100,6 @@ fn test_build_sequence_sets() {
async fn check_target_folder_combination( async fn check_target_folder_combination(
folder: &str, folder: &str,
mvbox_move: bool,
chat_msg: bool, chat_msg: bool,
expected_destination: &str, expected_destination: &str,
accepted_chat: bool, accepted_chat: bool,
@@ -108,16 +107,10 @@ async fn check_target_folder_combination(
setupmessage: bool, setupmessage: bool,
) -> Result<()> { ) -> Result<()> {
println!( 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; 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 { if accepted_chat {
let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?; let contact_id = Contact::create(&t.ctx, "", "bob@example.net").await?;
@@ -164,42 +157,33 @@ async fn check_target_folder_combination(
assert_eq!( assert_eq!(
expected, expected,
actual.as_deref(), 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(()) Ok(())
} }
// chat_msg means that the message was sent by Delta Chat // chat_msg means that the message was sent by Delta Chat
// The tuples are (folder, mvbox_move, chat_msg, expected_destination) // The tuples are (folder, chat_msg, expected_destination)
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[ const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, &str)] = &[
("INBOX", false, false, "INBOX"), ("INBOX", false, "INBOX"),
("INBOX", false, true, "INBOX"), ("INBOX", true, "INBOX"),
("INBOX", true, false, "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
("INBOX", true, true, "DeltaChat"), ("Spam", true, "INBOX"),
("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"),
]; ];
// These are the same as above, but non-chat messages in Spam stay in Spam // These are the same as above, but non-chat messages in Spam stay in Spam
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[ const COMBINATIONS_REQUEST: &[(&str, bool, &str)] = &[
("INBOX", false, false, "INBOX"), ("INBOX", false, "INBOX"),
("INBOX", false, true, "INBOX"), ("INBOX", true, "INBOX"),
("INBOX", true, false, "INBOX"), ("Spam", false, "Spam"),
("INBOX", true, true, "DeltaChat"), ("Spam", true, "INBOX"),
("Spam", false, false, "Spam"),
("Spam", false, true, "INBOX"),
("Spam", true, false, "Spam"),
("Spam", true, true, "DeltaChat"),
]; ];
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_target_folder_incoming_accepted() -> Result<()> { 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( check_target_folder_combination(
folder, folder,
*mvbox_move,
*chat_msg, *chat_msg,
expected_destination, expected_destination,
true, true,
@@ -213,10 +197,9 @@ async fn test_target_folder_incoming_accepted() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_target_folder_incoming_request() -> Result<()> { 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( check_target_folder_combination(
folder, folder,
*mvbox_move,
*chat_msg, *chat_msg,
expected_destination, expected_destination,
false, false,
@@ -231,17 +214,9 @@ async fn test_target_folder_incoming_request() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_target_folder_outgoing() -> Result<()> { async fn test_target_folder_outgoing() -> Result<()> {
// Test outgoing emails // Test outgoing emails
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( check_target_folder_combination(folder, *chat_msg, expected_destination, true, true, false)
folder, .await?;
*mvbox_move,
*chat_msg,
expected_destination,
true,
true,
false,
)
.await?;
} }
Ok(()) Ok(())
} }
@@ -249,10 +224,9 @@ async fn test_target_folder_outgoing() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_target_folder_setupmsg() -> Result<()> { async fn test_target_folder_setupmsg() -> Result<()> {
// Test setupmessages // 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( check_target_folder_combination(
folder, folder,
*mvbox_move,
*chat_msg, *chat_msg,
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam" if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
false, false,

View File

@@ -56,9 +56,37 @@ pub enum EnteredCertificateChecks {
AcceptInvalidCertificates2 = 3, 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)] #[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. /// Server hostname or IP address.
pub server: String, pub server: String,
@@ -86,10 +114,10 @@ pub struct EnteredLoginParam {
pub addr: String, pub addr: String,
/// IMAP settings. /// IMAP settings.
pub imap: EnteredServerLoginParam, pub imap: EnteredImapLoginParam,
/// SMTP settings. /// SMTP settings.
pub smtp: EnteredServerLoginParam, pub smtp: EnteredSmtpLoginParam,
/// TLS options: whether to allow invalid certificates and/or /// TLS options: whether to allow invalid certificates and/or
/// invalid hostnames /// invalid hostnames
@@ -101,6 +129,8 @@ pub struct EnteredLoginParam {
impl EnteredLoginParam { impl EnteredLoginParam {
/// Loads entered account settings. /// Loads entered account settings.
///
/// This is a legacy API for loading from separate config parameters.
pub(crate) async fn load(context: &Context) -> Result<Self> { pub(crate) async fn load(context: &Context) -> Result<Self> {
let addr = context let addr = context
.get_config(Config::Addr) .get_config(Config::Addr)
@@ -117,6 +147,10 @@ impl EnteredLoginParam {
.get_config_parsed::<u16>(Config::MailPort) .get_config_parsed::<u16>(Config::MailPort)
.await? .await?
.unwrap_or_default(); .unwrap_or_default();
// There is no way to set custom folder with this legacy API.
let mail_folder = String::new();
let mail_security = context let mail_security = context
.get_config_parsed::<i32>(Config::MailSecurity) .get_config_parsed::<i32>(Config::MailSecurity)
.await? .await?
@@ -175,14 +209,15 @@ impl EnteredLoginParam {
Ok(EnteredLoginParam { Ok(EnteredLoginParam {
addr, addr,
imap: EnteredServerLoginParam { imap: EnteredImapLoginParam {
server: mail_server, server: mail_server,
port: mail_port, port: mail_port,
folder: mail_folder,
security: mail_security, security: mail_security,
user: mail_user, user: mail_user,
password: mail_pw, password: mail_pw,
}, },
smtp: EnteredServerLoginParam { smtp: EnteredSmtpLoginParam {
server: send_server, server: send_server,
port: send_port, port: send_port,
security: send_security, security: send_security,
@@ -344,14 +379,15 @@ mod tests {
let t = TestContext::new().await; let t = TestContext::new().await;
let param = EnteredLoginParam { let param = EnteredLoginParam {
addr: "alice@example.org".to_string(), addr: "alice@example.org".to_string(),
imap: EnteredServerLoginParam { imap: EnteredImapLoginParam {
server: "".to_string(), server: "".to_string(),
port: 0, port: 0,
folder: "".to_string(),
security: Socket::Starttls, security: Socket::Starttls,
user: "".to_string(), user: "".to_string(),
password: "foobar".to_string(), password: "foobar".to_string(),
}, },
smtp: EnteredServerLoginParam { smtp: EnteredSmtpLoginParam {
server: "".to_string(), server: "".to_string(),
port: 2947, port: 2947,
security: Socket::default(), security: Socket::default(),

View File

@@ -17,7 +17,7 @@ use crate::config::Config;
use crate::contact::{Contact, ContactId, Origin}; use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context; use crate::context::Context;
use crate::key::Fingerprint; 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::http::post_empty;
use crate::net::proxy::{DEFAULT_SOCKS_PORT, ProxyConfig}; use crate::net::proxy::{DEFAULT_SOCKS_PORT, ProxyConfig};
use crate::token; use crate::token;
@@ -41,7 +41,7 @@ pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
/// Version written to Backups and Backup-QR-Codes. /// Version written to Backups and Backup-QR-Codes.
/// Imports will fail when they have a larger version. /// Imports will fail when they have a larger version.
pub(crate) const DCBACKUP_VERSION: i32 = 4; pub(crate) const DCBACKUP_VERSION: i32 = 5;
/// Scanned QR code. /// Scanned QR code.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -812,7 +812,7 @@ pub(crate) async fn login_param_from_account_qr(
let param = EnteredLoginParam { let param = EnteredLoginParam {
addr, addr,
imap: EnteredServerLoginParam { imap: EnteredImapLoginParam {
password, password,
..Default::default() ..Default::default()
}, },
@@ -832,7 +832,7 @@ pub(crate) async fn login_param_from_account_qr(
let param = EnteredLoginParam { let param = EnteredLoginParam {
addr: email, addr: email,
imap: EnteredServerLoginParam { imap: EnteredImapLoginParam {
password, password,
..Default::default() ..Default::default()
}, },

View File

@@ -5,7 +5,9 @@ use anyhow::{Context as _, Result, bail};
use deltachat_contact_tools::may_be_valid_addr; use deltachat_contact_tools::may_be_valid_addr;
use super::{DCLOGIN_SCHEME, Qr}; use super::{DCLOGIN_SCHEME, Qr};
use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam}; use crate::login_param::{
EnteredCertificateChecks, EnteredImapLoginParam, EnteredLoginParam, EnteredSmtpLoginParam,
};
use crate::provider::Socket; use crate::provider::Socket;
/// Options for `dclogin:` scheme. /// Options for `dclogin:` scheme.
@@ -178,14 +180,15 @@ pub(crate) fn login_param_from_login_qr(
} => { } => {
let param = EnteredLoginParam { let param = EnteredLoginParam {
addr: addr.to_string(), addr: addr.to_string(),
imap: EnteredServerLoginParam { imap: EnteredImapLoginParam {
server: imap_host.unwrap_or_default(), server: imap_host.unwrap_or_default(),
port: imap_port.unwrap_or_default(), port: imap_port.unwrap_or_default(),
folder: "INBOX".to_string(),
security: imap_security.unwrap_or_default(), security: imap_security.unwrap_or_default(),
user: imap_username.unwrap_or_default(), user: imap_username.unwrap_or_default(),
password: imap_password.unwrap_or(mail_pw), password: imap_password.unwrap_or(mail_pw),
}, },
smtp: EnteredServerLoginParam { smtp: EnteredSmtpLoginParam {
server: smtp_host.unwrap_or_default(), server: smtp_host.unwrap_or_default(),
port: smtp_port.unwrap_or_default(), port: smtp_port.unwrap_or_default(),
security: smtp_security.unwrap_or_default(), security: smtp_security.unwrap_or_default(),

View File

@@ -9,7 +9,6 @@ use async_imap::types::{Quota, QuotaResource};
use crate::chat::add_device_msg_with_importance; use crate::chat::add_device_msg_with_importance;
use crate::config::Config; use crate::config::Config;
use crate::context::Context; use crate::context::Context;
use crate::imap::get_watched_folders;
use crate::imap::session::Session as ImapSession; use crate::imap::session::Session as ImapSession;
use crate::log::warn; use crate::log::warn;
use crate::message::Message; use crate::message::Message;
@@ -48,26 +47,24 @@ pub struct QuotaInfo {
async fn get_unique_quota_roots_and_usage( async fn get_unique_quota_roots_and_usage(
session: &mut ImapSession, session: &mut ImapSession,
folders: Vec<String>, folder: String,
) -> Result<BTreeMap<String, Vec<QuotaResource>>> { ) -> Result<BTreeMap<String, Vec<QuotaResource>>> {
let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new(); let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new();
for folder in folders { let (quota_roots, quotas) = &session.get_quota_root(&folder).await?;
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
// if there are new quota roots found in this imap folder, add them to the list for qr_entries in quota_roots {
for qr_entries in quota_roots { for quota_root_name in &qr_entries.quota_root_names {
for quota_root_name in &qr_entries.quota_root_names { // the quota for that quota root
// the quota for that quota root let quota: Quota = quotas
let quota: Quota = quotas .iter()
.iter() .find(|q| &q.root_name == quota_root_name)
.find(|q| &q.root_name == quota_root_name) .cloned()
.cloned() .context("quota_root should have a quota")?;
.context("quota_root should have a quota")?; // replace old quotas, because between fetching quotaroots for folders,
// replace old quotas, because between fetching quotaroots for folders, // messages could be received and so the usage could have been changed
// messages could be received and so the usage could have been changed *unique_quota_roots
*unique_quota_roots .entry(quota_root_name.clone())
.entry(quota_root_name.clone()) .or_default() = quota.resources;
.or_default() = quota.resources;
}
} }
} }
Ok(unique_quota_roots) Ok(unique_quota_roots)
@@ -123,10 +120,13 @@ impl Context {
/// As the message is added only once, the user is not spammed /// As the message is added only once, the user is not spammed
/// in case for some providers the quota is always at ~100% /// in case for some providers the quota is always at ~100%
/// and new space is allocated as needed. /// 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 quota = if session.can_check_quota() {
let folders = get_watched_folders(self).await?; get_unique_quota_roots_and_usage(session, folder).await
get_unique_quota_roots_and_usage(session, folders).await
} else { } else {
Err(anyhow!(stock_str::not_supported_by_provider(self).await)) Err(anyhow!(stock_str::not_supported_by_provider(self).await))
}; };

View File

@@ -17,7 +17,7 @@ use crate::context::Context;
use crate::download::{download_known_post_messages_without_pre_message, download_msgs}; use crate::download::{download_known_post_messages_without_pre_message, download_msgs};
use crate::ephemeral::{self, delete_expired_imap_messages}; use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::events::EventType; use crate::events::EventType;
use crate::imap::{FolderMeaning, Imap, session::Session}; use crate::imap::{Imap, session::Session};
use crate::location; use crate::location;
use crate::log::{LogExt, warn}; use crate::log::{LogExt, warn};
use crate::smtp::{Smtp, send_smtp_messages}; use crate::smtp::{Smtp, send_smtp_messages};
@@ -211,25 +211,19 @@ impl SchedulerState {
/// Indicate that the network likely has come back. /// Indicate that the network likely has come back.
pub(crate) async fn maybe_network(&self) { pub(crate) async fn maybe_network(&self) {
let inner = self.inner.read().await; let inner = self.inner.read().await;
let (inboxes, oboxes) = match *inner { let inboxes = match *inner {
InnerSchedulerState::Started(ref scheduler) => { InnerSchedulerState::Started(ref scheduler) => {
scheduler.maybe_network(); scheduler.maybe_network();
let inboxes = scheduler scheduler
.inboxes .inboxes
.iter() .iter()
.map(|b| b.conn_state.state.connectivity.clone()) .map(|b| b.conn_state.state.connectivity.clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>()
let oboxes = scheduler
.oboxes
.iter()
.map(|b| b.conn_state.state.connectivity.clone())
.collect::<Vec<_>>();
(inboxes, oboxes)
} }
_ => return, _ => return,
}; };
drop(inner); drop(inner);
connectivity::idle_interrupted(inboxes, oboxes); connectivity::idle_interrupted(inboxes);
} }
/// Indicate that the network likely is lost. /// Indicate that the network likely is lost.
@@ -318,7 +312,10 @@ impl Drop for IoPausedGuard {
struct SchedBox { struct SchedBox {
/// Address at the used chatmail/email relay /// Address at the used chatmail/email relay
addr: String, addr: String,
meaning: FolderMeaning,
/// Folder name
folder: String,
conn_state: ImapConnectionState, conn_state: ImapConnectionState,
/// IMAP loop task handle. /// IMAP loop task handle.
@@ -330,8 +327,6 @@ struct SchedBox {
pub(crate) struct Scheduler { pub(crate) struct Scheduler {
/// Inboxes, one per transport. /// Inboxes, one per transport.
inboxes: Vec<SchedBox>, inboxes: Vec<SchedBox>,
/// Optional boxes -- mvbox.
oboxes: Vec<SchedBox>,
smtp: SmtpConnectionState, smtp: SmtpConnectionState,
smtp_handle: task::JoinHandle<()>, smtp_handle: task::JoinHandle<()>,
ephemeral_handle: task::JoinHandle<()>, ephemeral_handle: task::JoinHandle<()>,
@@ -400,40 +395,11 @@ async fn inbox_loop(
.await; .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<Option<(Config, String)>> {
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<Session> { async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result<Session> {
let folder = imap.folder.clone();
// Update quota no more than once a minute. // Update quota no more than once a minute.
if ctx.quota_needs_update(session.transport_id(), 60).await 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); warn!(ctx, "Failed to update quota: {:#}.", err);
} }
@@ -471,7 +437,7 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
.await .await
.context("Failed to register push token")?; .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) Ok(session)
} }
@@ -480,32 +446,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 /// 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, /// handling all the errors. In case of an error, an error is returned and connection is dropped,
/// otherwise connection is returned. /// otherwise connection is returned.
async fn fetch_idle( async fn fetch_idle(ctx: &Context, connection: &mut Imap, mut session: Session) -> Result<Session> {
ctx: &Context, let watch_folder = connection.folder.clone();
connection: &mut Imap,
mut session: Session,
folder_meaning: FolderMeaning,
) -> Result<Session> {
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");
};
if folder_config == Config::ConfiguredInboxFolder { session
session .store_seen_flags_on_imap(ctx)
.store_seen_flags_on_imap(ctx) .await
.await .context("store_seen_flags_on_imap")?;
.context("store_seen_flags_on_imap")?;
}
// Fetch the watched folder. // Fetch the watched folder.
connection connection
.fetch_move_delete(ctx, &mut session, &watch_folder, folder_meaning) .fetch_move_delete(ctx, &mut session, &watch_folder)
.await .await
.context("fetch_move_delete")?; .context("fetch_move_delete")?;
@@ -539,7 +490,7 @@ async fn fetch_idle(
ctx, ctx,
"IMAP session does not support IDLE, going to fake idle." "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); return Ok(session);
} }
@@ -551,7 +502,7 @@ async fn fetch_idle(
.unwrap_or_default() .unwrap_or_default()
{ {
info!(ctx, "IMAP IDLE is disabled, going to fake idle."); 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); return Ok(session);
} }
@@ -571,73 +522,6 @@ async fn fetch_idle(
Ok(session) 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<Session> = 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( async fn smtp_loop(
ctx: Context, ctx: Context,
started: oneshot::Sender<()>, started: oneshot::Sender<()>,
@@ -740,7 +624,6 @@ impl Scheduler {
let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1); let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1);
let mut inboxes = Vec::new(); let mut inboxes = Vec::new();
let mut oboxes = Vec::new();
let mut start_recvs = Vec::new(); let mut start_recvs = Vec::new();
for (transport_id, configured_login_param) in ConfiguredLoginParam::load_all(ctx).await? { for (transport_id, configured_login_param) in ConfiguredLoginParam::load_all(ctx).await? {
@@ -752,30 +635,17 @@ impl Scheduler {
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers)) task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
}; };
let addr = configured_login_param.addr.clone(); let addr = configured_login_param.addr.clone();
let folder = configured_login_param
.imap_folder
.unwrap_or_else(|| "INBOX".to_string());
let inbox = SchedBox { let inbox = SchedBox {
addr: addr.clone(), addr: addr.clone(),
meaning: FolderMeaning::Inbox, folder,
conn_state, conn_state,
handle, handle,
}; };
inboxes.push(inbox); inboxes.push(inbox);
start_recvs.push(inbox_start_recv); 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 = { let smtp_handle = {
@@ -802,7 +672,6 @@ impl Scheduler {
let res = Self { let res = Self {
inboxes, inboxes,
oboxes,
smtp, smtp,
smtp_handle, smtp_handle,
ephemeral_handle, ephemeral_handle,
@@ -822,7 +691,7 @@ impl Scheduler {
} }
fn boxes(&self) -> impl Iterator<Item = &SchedBox> { fn boxes(&self) -> impl Iterator<Item = &SchedBox> {
self.inboxes.iter().chain(self.oboxes.iter()) self.inboxes.iter()
} }
fn maybe_network(&self) { fn maybe_network(&self) {
@@ -876,7 +745,7 @@ impl Scheduler {
let timeout_duration = std::time::Duration::from_secs(30); let timeout_duration = std::time::Duration::from_secs(30);
let tracker = TaskTracker::new(); 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(); let context = context.clone();
tracker.spawn(async move { tracker.spawn(async move {
tokio::time::timeout(timeout_duration, b.handle) tokio::time::timeout(timeout_duration, b.handle)

View File

@@ -5,11 +5,10 @@ use std::{iter::once, ops::Deref, sync::Arc};
use anyhow::Result; use anyhow::Result;
use humansize::{BINARY, format_size}; use humansize::{BINARY, format_size};
use crate::context::Context;
use crate::events::EventType; 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::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
use crate::stock_str; use crate::stock_str;
use crate::{context::Context, log::LogExt};
use super::InnerSchedulerState; use super::InnerSchedulerState;
@@ -67,40 +66,33 @@ enum DetailedConnectivity {
/// Connection is established and is idle. /// Connection is established and is idle.
Idle, Idle,
/// The folder was configured not to be watched or configured_*_folder is not set
NotConfigured,
} }
impl DetailedConnectivity { impl DetailedConnectivity {
fn to_basic(&self) -> Option<Connectivity> { fn to_basic(&self) -> Connectivity {
match self { match self {
DetailedConnectivity::Error(_) => Some(Connectivity::NotConnected), DetailedConnectivity::Error(_) => Connectivity::NotConnected,
DetailedConnectivity::Uninitialized => Some(Connectivity::NotConnected), DetailedConnectivity::Uninitialized => Connectivity::NotConnected,
DetailedConnectivity::Connecting => Some(Connectivity::Connecting), DetailedConnectivity::Connecting => Connectivity::Connecting,
DetailedConnectivity::Working => Some(Connectivity::Working), DetailedConnectivity::Working => Connectivity::Working,
DetailedConnectivity::InterruptingIdle => Some(Connectivity::Working), DetailedConnectivity::InterruptingIdle => Connectivity::Working,
// At this point IMAP has just connected, // At this point IMAP has just connected,
// but does not know yet if there are messages to download. // but does not know yet if there are messages to download.
// We still convert this to Working state // We still convert this to Working state
// so user can see "Updating..." and not "Connected" // so user can see "Updating..." and not "Connected"
// which is reserved for idle state. // which is reserved for idle state.
DetailedConnectivity::Preparing => Some(Connectivity::Working), DetailedConnectivity::Preparing => Connectivity::Working,
// Just don't return a connectivity, probably the folder is configured not to be DetailedConnectivity::Idle => Connectivity::Connected,
// watched, so we are not interested in it.
DetailedConnectivity::NotConfigured => None,
DetailedConnectivity::Idle => Some(Connectivity::Connected),
} }
} }
fn to_icon(&self) -> String { fn to_icon(&self) -> String {
match self { match self {
DetailedConnectivity::Error(_) DetailedConnectivity::Error(_) | DetailedConnectivity::Uninitialized => {
| DetailedConnectivity::Uninitialized "<span class=\"red dot\"></span>".to_string()
| DetailedConnectivity::NotConfigured => "<span class=\"red dot\"></span>".to_string(), }
DetailedConnectivity::Connecting => "<span class=\"yellow dot\"></span>".to_string(), DetailedConnectivity::Connecting => "<span class=\"yellow dot\"></span>".to_string(),
DetailedConnectivity::Preparing DetailedConnectivity::Preparing
| DetailedConnectivity::Working | DetailedConnectivity::Working
@@ -120,7 +112,6 @@ impl DetailedConnectivity {
DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => { DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => {
stock_str::connected(context).await stock_str::connected(context).await
} }
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
} }
} }
@@ -139,7 +130,6 @@ impl DetailedConnectivity {
DetailedConnectivity::InterruptingIdle DetailedConnectivity::InterruptingIdle
| DetailedConnectivity::Preparing | DetailedConnectivity::Preparing
| DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context).await, | 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::Working => false,
DetailedConnectivity::InterruptingIdle => false, DetailedConnectivity::InterruptingIdle => false,
DetailedConnectivity::Preparing => false, // Just connected, there may still be work to do. DetailedConnectivity::Preparing => false, // Just connected, there may still be work to do.
DetailedConnectivity::NotConfigured => true,
DetailedConnectivity::Idle => true, DetailedConnectivity::Idle => true,
} }
} }
@@ -180,9 +169,6 @@ impl ConnectivityStore {
pub(crate) fn set_preparing(&self, context: &Context) { pub(crate) fn set_preparing(&self, context: &Context) {
self.set(context, DetailedConnectivity::Preparing); 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) { pub(crate) fn set_idle(&self, context: &Context) {
self.set(context, DetailedConnectivity::Idle); self.set(context, DetailedConnectivity::Idle);
} }
@@ -190,7 +176,7 @@ impl ConnectivityStore {
fn get_detailed(&self) -> DetailedConnectivity { fn get_detailed(&self) -> DetailedConnectivity {
self.0.lock().deref().clone() self.0.lock().deref().clone()
} }
fn get_basic(&self) -> Option<Connectivity> { fn get_basic(&self) -> Connectivity {
self.0.lock().to_basic() self.0.lock().to_basic()
} }
fn get_all_work_done(&self) -> bool { 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. /// Set all folder states to InterruptingIdle in case they were `Idle` before.
/// Called during `dc_maybe_network()` to make sure that `all_work_done()` /// Called during `dc_maybe_network()` to make sure that `all_work_done()`
/// returns false immediately after `dc_maybe_network()`. /// returns false immediately after `dc_maybe_network()`.
pub(crate) fn idle_interrupted(inboxes: Vec<ConnectivityStore>, oboxes: Vec<ConnectivityStore>) { pub(crate) fn idle_interrupted(inboxes: Vec<ConnectivityStore>) {
for inbox in inboxes { for inbox in inboxes {
let mut connectivity_lock = inbox.0.lock(); 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 { if *connectivity_lock == DetailedConnectivity::Idle {
*connectivity_lock = DetailedConnectivity::InterruptingIdle; *connectivity_lock = DetailedConnectivity::InterruptingIdle;
} }
} }
// No need to send ConnectivityChanged, the user-facing connectivity doesn't change because // No need to send ConnectivityChanged, the user-facing connectivity doesn't change because
// of what we do here. // of what we do here.
} }
@@ -234,9 +207,7 @@ pub(crate) fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStor
let mut connectivity_lock = store.0.lock(); let mut connectivity_lock = store.0.lock();
if !matches!( if !matches!(
*connectivity_lock, *connectivity_lock,
DetailedConnectivity::Uninitialized DetailedConnectivity::Uninitialized | DetailedConnectivity::Error(_)
| DetailedConnectivity::Error(_)
| DetailedConnectivity::NotConfigured,
) { ) {
*connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string()); *connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string());
} }
@@ -273,9 +244,8 @@ impl Context {
let stores = self.connectivities.lock().clone(); let stores = self.connectivities.lock().clone();
let mut connectivities = Vec::new(); let mut connectivities = Vec::new();
for s in stores { for s in stores {
if let Some(connectivity) = s.get_basic() { let connectivity = s.get_basic();
connectivities.push(connectivity); connectivities.push(connectivity);
}
} }
connectivities connectivities
.into_iter() .into_iter()
@@ -386,7 +356,7 @@ impl Context {
.map(|b| { .map(|b| {
( (
b.addr.clone(), b.addr.clone(),
b.meaning, b.folder.clone(),
b.conn_state.state.connectivity.clone(), b.conn_state.state.connectivity.clone(),
) )
}) })
@@ -411,7 +381,6 @@ impl Context {
// [======67%===== ] // [======67%===== ]
// ============================================================================================= // =============================================================================================
let watched_folders = get_watched_folder_configs(self).await?;
let incoming_messages = stock_str::incoming_messages(self).await; let incoming_messages = stock_str::incoming_messages(self).await;
ret += &format!("<h3>{incoming_messages}</h3><ul>"); ret += &format!("<h3>{incoming_messages}</h3><ul>");
@@ -433,41 +402,14 @@ impl Context {
let folders = folders_states let folders = folders_states
.iter() .iter()
.filter(|(folder_addr, ..)| *folder_addr == transport_addr); .filter(|(folder_addr, ..)| *folder_addr == transport_addr);
for (_addr, folder, state) in folders { for (_addr, _folder, state) in folders {
let mut folder_added = false; let detailed = &state.get_detailed();
ret += &*detailed.to_icon();
if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) { ret += " <b>";
let f = self.get_config(config).await.log_err(self).ok().flatten(); ret += &*domain_escaped;
ret += ":</b> ";
if let Some(foldername) = f { ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
let detailed = &state.get_detailed(); ret += "<br />";
ret += &*detailed.to_icon();
ret += " <b>";
if folder == &FolderMeaning::Inbox {
ret += &*domain_escaped;
} else {
ret += &*escaper::encode_minimal(&foldername);
}
ret += ":</b> ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += "<br />";
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 += "<br />";
}
}
} }
let Some(quota) = quota.get(&transport_id) else { let Some(quota) = quota.get(&transport_id) else {

View File

@@ -9,7 +9,6 @@ use rusqlite::{Connection, OpenFlags, Row, config::DbConfig, types::ValueRef};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::blob::BlobObject; use crate::blob::BlobObject;
use crate::chat::add_device_msg;
use crate::config::Config; use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH; use crate::constants::DC_CHAT_ID_TRASH;
use crate::context::Context; use crate::context::Context;
@@ -18,13 +17,11 @@ use crate::ephemeral::start_ephemeral_timers;
use crate::imex::BLOBS_BACKUP_NAME; use crate::imex::BLOBS_BACKUP_NAME;
use crate::location::delete_orphaned_poi_locations; use crate::location::delete_orphaned_poi_locations;
use crate::log::{LogExt, warn}; use crate::log::{LogExt, warn};
use crate::message::Message;
use crate::message::MsgId; use crate::message::MsgId;
use crate::net::dns::prune_dns_cache; use crate::net::dns::prune_dns_cache;
use crate::net::http::http_cache_cleanup; use crate::net::http::http_cache_cleanup;
use crate::net::prune_connection_history; use crate::net::prune_connection_history;
use crate::param::{Param, Params}; use crate::param::{Param, Params};
use crate::stock_str;
use crate::tools::{SystemTime, delete_file, time}; use crate::tools::{SystemTime, delete_file, time};
/// Extension to [`rusqlite::ToSql`] trait /// Extension to [`rusqlite::ToSql`] trait
@@ -830,12 +827,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 { if let Err(err) = incremental_vacuum(context).await {
warn!(context, "Failed to run incremental vacuum: {err:#}."); warn!(context, "Failed to run incremental vacuum: {err:#}.");
} }
@@ -895,18 +886,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
Ok(()) 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<u8>`. /// Get the value of a column `idx` of the `row` as `Vec<u8>`.
pub fn row_get_vec(row: &Row, idx: usize) -> rusqlite::Result<Vec<u8>> { pub fn row_get_vec(row: &Row, idx: usize) -> rusqlite::Result<Vec<u8>> {
row.get(idx).or_else(|err| match row.get_ref(idx)? { row.get(idx).or_else(|err| match row.get_ref(idx)? {

View File

@@ -2323,8 +2323,52 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
.await?; .await?;
} }
// Add UNIQUE bound to token, in order to avoid saving the same token multiple times
inc_and_check(&mut migration_version, 148)?; inc_and_check(&mut migration_version, 148)?;
if dbversion < migration_version {
sql.execute_migration_transaction(
|transaction| {
let only_fetch_mvbox = transaction
.query_row(
"SELECT value FROM config WHERE keyname='only_fetch_mvbox'",
(),
|row| {
let value: String = row.get(0)?;
Ok(value)
},
)
.optional()?
.as_deref()
== Some("1");
if only_fetch_mvbox {
let mvbox_folder = transaction
.query_row(
"SELECT value FROM config WHERE keyname='configured_mvbox_folder'",
(),
|row| {
let value: String = row.get(0)?;
Ok(value)
},
)
.optional()?
.unwrap_or_else(|| "DeltaChat".to_string());
transaction.execute(
"UPDATE transports
SET entered_param=json_set(entered_param, '$.imap.folder', ?1),
configured_param=json_set(configured_param', '$.imap_folder', ?1)",
(mvbox_folder,),
)?;
}
Ok(())
},
migration_version,
)
.await?;
}
// Add UNIQUE bound to token, in order to avoid saving the same token multiple times
inc_and_check(&mut migration_version, 149)?;
if dbversion < migration_version { if dbversion < migration_version {
sql.execute_migration( sql.execute_migration(
"CREATE TABLE tokens_new ( "CREATE TABLE tokens_new (

View File

@@ -413,11 +413,6 @@ https://delta.chat/donate"))]
#[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))] #[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))]
ChatUnencryptedExplanation = 230, 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"))] #[strum(props(fallback = "Outgoing audio call"))]
OutgoingAudioCall = 232, OutgoingAudioCall = 232,
@@ -1296,11 +1291,6 @@ pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
translated(context, StockMessage::ChatUnencryptedExplanation).await 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 { impl Viewtype {
/// returns Localized name for message viewtype /// returns Localized name for message viewtype
pub async fn to_locale_string(&self, context: &Context) -> String { pub async fn to_locale_string(&self, context: &Context) -> String {

View File

@@ -567,7 +567,6 @@ impl TestContext {
.unwrap(); .unwrap();
ctx.set_config(Config::BccSelf, Some("1")).await.unwrap(); ctx.set_config(Config::BccSelf, Some("1")).await.unwrap();
ctx.set_config(Config::SyncMsgs, Some("0")).await.unwrap(); ctx.set_config(Config::SyncMsgs, Some("0")).await.unwrap();
ctx.set_config(Config::MvboxMove, Some("0")).await.unwrap();
Self { Self {
ctx, ctx,

View File

@@ -19,6 +19,7 @@ use crate::config::Config;
use crate::configure::server_params::{ServerParams, expand_param_vector}; use crate::configure::server_params::{ServerParams, expand_param_vector};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2}; use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
use crate::context::Context; use crate::context::Context;
use crate::ensure_and_debug_assert;
use crate::events::EventType; use crate::events::EventType;
use crate::login_param::EnteredLoginParam; use crate::login_param::EnteredLoginParam;
use crate::net::load_connection_timestamp; use crate::net::load_connection_timestamp;
@@ -163,22 +164,30 @@ pub(crate) struct ConfiguredLoginParam {
/// `From:` address that was used at the time of configuration. /// `From:` address that was used at the time of configuration.
pub addr: String, pub addr: String,
/// List of IMAP candidates to try.
pub imap: Vec<ConfiguredServerLoginParam>, pub imap: Vec<ConfiguredServerLoginParam>,
// Custom IMAP user. /// Custom IMAP user.
// ///
// This overwrites autoconfig from the provider database /// This overwrites autoconfig from the provider database
// if non-empty. /// if non-empty.
pub imap_user: String, pub imap_user: String,
pub imap_password: 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<String>,
/// List of SMTP candidates to try.
pub smtp: Vec<ConfiguredServerLoginParam>, pub smtp: Vec<ConfiguredServerLoginParam>,
// Custom SMTP user. /// Custom SMTP user.
// ///
// This overwrites autoconfig from the provider database /// This overwrites autoconfig from the provider database
// if non-empty. /// if non-empty.
pub smtp_user: String, pub smtp_user: String,
pub smtp_password: String, pub smtp_password: String,
@@ -199,6 +208,13 @@ pub(crate) struct ConfiguredLoginParam {
pub(crate) struct ConfiguredLoginParamJson { pub(crate) struct ConfiguredLoginParamJson {
pub addr: String, pub addr: String,
pub imap: Vec<ConfiguredServerLoginParam>, pub imap: Vec<ConfiguredServerLoginParam>,
/// IMAP folder to watch.
///
/// Defaults to "INBOX" if unset.
#[serde(skip_serializing_if = "Option::is_none")]
pub imap_folder: Option<String>,
pub imap_user: String, pub imap_user: String,
pub imap_password: String, pub imap_password: String,
pub smtp: Vec<ConfiguredServerLoginParam>, pub smtp: Vec<ConfiguredServerLoginParam>,
@@ -545,6 +561,7 @@ impl ConfiguredLoginParam {
Ok(Some(ConfiguredLoginParam { Ok(Some(ConfiguredLoginParam {
addr, addr,
imap, imap,
imap_folder: None,
imap_user: mail_user, imap_user: mail_user,
imap_password: mail_pw, imap_password: mail_pw,
smtp, smtp,
@@ -569,11 +586,18 @@ impl ConfiguredLoginParam {
pub(crate) fn from_json(json: &str) -> Result<Self> { pub(crate) fn from_json(json: &str) -> Result<Self> {
let json: ConfiguredLoginParamJson = serde_json::from_str(json)?; 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)); let provider = json.provider_id.and_then(|id| get_provider_by_id(&id));
Ok(ConfiguredLoginParam { Ok(ConfiguredLoginParam {
addr: json.addr, addr: json.addr,
imap: json.imap, imap: json.imap,
imap_folder: json.imap_folder,
imap_user: json.imap_user, imap_user: json.imap_user,
imap_password: json.imap_password, imap_password: json.imap_password,
smtp: json.smtp, smtp: json.smtp,
@@ -611,6 +635,7 @@ impl From<ConfiguredLoginParam> for ConfiguredLoginParamJson {
imap: configured_login_param.imap, imap: configured_login_param.imap,
imap_user: configured_login_param.imap_user, imap_user: configured_login_param.imap_user,
imap_password: configured_login_param.imap_password, imap_password: configured_login_param.imap_password,
imap_folder: configured_login_param.imap_folder,
smtp: configured_login_param.smtp, smtp: configured_login_param.smtp,
smtp_user: configured_login_param.smtp_user, smtp_user: configured_login_param.smtp_user,
smtp_password: configured_login_param.smtp_password, smtp_password: configured_login_param.smtp_password,
@@ -629,9 +654,16 @@ pub(crate) async fn save_transport(
configured: &ConfiguredLoginParamJson, configured: &ConfiguredLoginParamJson,
add_timestamp: i64, add_timestamp: i64,
) -> Result<bool> { ) -> Result<bool> {
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 addr = addr_normalize(&configured.addr);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?; let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
let mut modified = context let mut modified = context
.sql .sql
.execute( .execute(
@@ -824,6 +856,7 @@ mod tests {
}, },
user: "alice".to_string(), user: "alice".to_string(),
}], }],
imap_folder: None,
imap_user: "".to_string(), imap_user: "".to_string(),
imap_password: "foo".to_string(), imap_password: "foo".to_string(),
smtp: vec![ConfiguredServerLoginParam { smtp: vec![ConfiguredServerLoginParam {
@@ -932,6 +965,7 @@ mod tests {
user: user.to_string(), user: user.to_string(),
}, },
], ],
imap_folder: None,
imap_user: "alice@posteo.de".to_string(), imap_user: "alice@posteo.de".to_string(),
imap_password: "foobarbaz".to_string(), imap_password: "foobarbaz".to_string(),
smtp: vec![ smtp: vec![
@@ -1045,6 +1079,7 @@ mod tests {
}, },
user: addr.clone(), user: addr.clone(),
}], }],
imap_folder: None,
imap_user: addr.clone(), imap_user: addr.clone(),
imap_password: "foobarbaz".to_string(), imap_password: "foobarbaz".to_string(),
smtp: vec![ConfiguredServerLoginParam { smtp: vec![ConfiguredServerLoginParam {