mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
feat: move the messages only from INBOX and Spam folders
We do not try to delete resent messages anymore. Previously resent messages were distinguised by having duplicate Message-ID, but future Date, but now we need to download the message before we even see the Date. We now move the message to the destination folder but do not fetch it. It may not be a good idea to delete the duplicate in multi-device setups anyway, because the device which has a message may delete the duplicate of a message the other device missed. To avoid triggering IMAP busy move loop described in the comments we now only move the messages from INBOX and Spam folders.
This commit is contained in:
@@ -308,7 +308,7 @@ def test_move_works(acfactory):
|
|||||||
|
|
||||||
|
|
||||||
def test_move_avoids_loop(acfactory):
|
def test_move_avoids_loop(acfactory):
|
||||||
"""Test that the message is only moved once.
|
"""Test that the message is only moved from INBOX to DeltaChat.
|
||||||
|
|
||||||
This is to avoid busy loop if moved message reappears in the Inbox
|
This is to avoid busy loop if moved message reappears in the Inbox
|
||||||
or some scanned folder later.
|
or some scanned folder later.
|
||||||
@@ -319,6 +319,14 @@ def test_move_avoids_loop(acfactory):
|
|||||||
ac1 = acfactory.new_online_configuring_account()
|
ac1 = acfactory.new_online_configuring_account()
|
||||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||||
acfactory.bring_accounts_online()
|
acfactory.bring_accounts_online()
|
||||||
|
|
||||||
|
# Create INBOX.DeltaChat folder and make sure
|
||||||
|
# it is detected by full folder scan.
|
||||||
|
ac2.direct_imap.create_folder("INBOX.DeltaChat")
|
||||||
|
ac2.stop_io()
|
||||||
|
ac2.start_io()
|
||||||
|
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
|
||||||
|
|
||||||
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
|
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||||
ac1_chat.send_text("Message 1")
|
ac1_chat.send_text("Message 1")
|
||||||
|
|
||||||
@@ -326,20 +334,28 @@ def test_move_avoids_loop(acfactory):
|
|||||||
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
|
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||||
assert ac2_msg1.text == "Message 1"
|
assert ac2_msg1.text == "Message 1"
|
||||||
|
|
||||||
# Move the message to the INBOX again.
|
# Move the message to the INBOX.DeltaChat again.
|
||||||
|
# We assume that test server uses "." as the delimiter.
|
||||||
ac2.direct_imap.select_folder("DeltaChat")
|
ac2.direct_imap.select_folder("DeltaChat")
|
||||||
ac2.direct_imap.conn.move(["*"], "INBOX")
|
ac2.direct_imap.conn.move(["*"], "INBOX.DeltaChat")
|
||||||
|
|
||||||
ac1_chat.send_text("Message 2")
|
ac1_chat.send_text("Message 2")
|
||||||
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
|
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||||
assert ac2_msg2.text == "Message 2"
|
assert ac2_msg2.text == "Message 2"
|
||||||
|
|
||||||
# Check that Message 1 is still in the INBOX folder
|
# Stop and start I/O to trigger folder scan.
|
||||||
|
ac2.stop_io()
|
||||||
|
ac2.start_io()
|
||||||
|
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
|
||||||
|
|
||||||
|
# Check that Message 1 is still in the INBOX.DeltaChat folder
|
||||||
# and Message 2 is in the DeltaChat folder.
|
# and Message 2 is in the DeltaChat folder.
|
||||||
ac2.direct_imap.select_folder("INBOX")
|
ac2.direct_imap.select_folder("INBOX")
|
||||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
assert len(ac2.direct_imap.get_all_messages()) == 0
|
||||||
ac2.direct_imap.select_folder("DeltaChat")
|
ac2.direct_imap.select_folder("DeltaChat")
|
||||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||||
|
ac2.direct_imap.select_folder("INBOX.DeltaChat")
|
||||||
|
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_move_works_on_self_sent(acfactory):
|
def test_move_works_on_self_sent(acfactory):
|
||||||
@@ -450,14 +466,19 @@ def test_resend_message(acfactory, lp):
|
|||||||
lp.sec("ac2: receive message")
|
lp.sec("ac2: receive message")
|
||||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||||
assert msg_in.text == "message"
|
assert msg_in.text == "message"
|
||||||
chat2 = msg_in.chat
|
|
||||||
chat2_msg_cnt = len(chat2.get_messages())
|
|
||||||
|
|
||||||
lp.sec("ac1: resend message")
|
lp.sec("ac1: resend message")
|
||||||
ac1.resend_messages([msg_in])
|
ac1.resend_messages([msg_in])
|
||||||
|
|
||||||
lp.sec("ac2: check that message is deleted")
|
lp.sec("ac1: send another message")
|
||||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
chat1.send_text("another message")
|
||||||
|
|
||||||
|
lp.sec("ac2: receive another message")
|
||||||
|
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||||
|
assert msg_in.text == "another message"
|
||||||
|
chat2 = msg_in.chat
|
||||||
|
chat2_msg_cnt = len(chat2.get_messages())
|
||||||
|
|
||||||
assert len(chat2.get_messages()) == chat2_msg_cnt
|
assert len(chat2.get_messages()) == chat2_msg_cnt
|
||||||
|
|
||||||
|
|
||||||
@@ -1757,10 +1778,10 @@ def test_group_quote(acfactory, lp):
|
|||||||
"xyz",
|
"xyz",
|
||||||
), # Test that emails aren't found in a random folder
|
), # Test that emails aren't found in a random folder
|
||||||
(
|
(
|
||||||
"Spam",
|
"xyz",
|
||||||
True,
|
True,
|
||||||
"DeltaChat",
|
"xyz",
|
||||||
), # ...emails are moved from the spam folder to "DeltaChat"
|
), # ...emails are found in a random folder and downloaded without moving
|
||||||
(
|
(
|
||||||
"Spam",
|
"Spam",
|
||||||
False,
|
False,
|
||||||
|
|||||||
@@ -737,9 +737,6 @@ impl Context {
|
|||||||
stats::pre_sending_config_change(self, old_value, new_value).await?;
|
stats::pre_sending_config_change(self, old_value, new_value).await?;
|
||||||
}
|
}
|
||||||
self.set_config_internal(key, value).await?;
|
self.set_config_internal(key, value).await?;
|
||||||
if key == Config::SentboxWatch {
|
|
||||||
self.last_full_folder_scan.lock().await.take();
|
|
||||||
}
|
|
||||||
if key == Config::StatsSending {
|
if key == Config::StatsSending {
|
||||||
stats::maybe_send_stats(self).await?;
|
stats::maybe_send_stats(self).await?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,8 +258,6 @@ pub struct InnerContext {
|
|||||||
/// IMAP METADATA.
|
/// IMAP METADATA.
|
||||||
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
|
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
|
||||||
|
|
||||||
pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
|
|
||||||
|
|
||||||
/// ID for this `Context` in the current process.
|
/// ID for this `Context` in the current process.
|
||||||
///
|
///
|
||||||
/// This allows for multiple `Context`s open in a single process where each context can
|
/// This allows for multiple `Context`s open in a single process where each context can
|
||||||
@@ -468,7 +466,6 @@ impl Context {
|
|||||||
server_id: RwLock::new(None),
|
server_id: RwLock::new(None),
|
||||||
metadata: RwLock::new(None),
|
metadata: RwLock::new(None),
|
||||||
creation_time: tools::Time::now(),
|
creation_time: tools::Time::now(),
|
||||||
last_full_folder_scan: Mutex::new(None),
|
|
||||||
last_error: parking_lot::RwLock::new("".to_string()),
|
last_error: parking_lot::RwLock::new("".to_string()),
|
||||||
migration_error: parking_lot::RwLock::new(None),
|
migration_error: parking_lot::RwLock::new(None),
|
||||||
debug_logging: std::sync::RwLock::new(None),
|
debug_logging: std::sync::RwLock::new(None),
|
||||||
|
|||||||
@@ -157,30 +157,23 @@ pub(crate) async fn download_msg(
|
|||||||
let row = context
|
let row = context
|
||||||
.sql
|
.sql
|
||||||
.query_row_optional(
|
.query_row_optional(
|
||||||
"SELECT uid, folder, uidvalidity FROM imap WHERE rfc724_mid=? AND target!=''",
|
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
|
||||||
(&msg.rfc724_mid,),
|
(&msg.rfc724_mid,),
|
||||||
|row| {
|
|row| {
|
||||||
let server_uid: u32 = row.get(0)?;
|
let server_uid: u32 = row.get(0)?;
|
||||||
let server_folder: String = row.get(1)?;
|
let server_folder: String = row.get(1)?;
|
||||||
let uidvalidity: u32 = row.get(2)?;
|
Ok((server_uid, server_folder))
|
||||||
Ok((server_uid, server_folder, uidvalidity))
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let Some((server_uid, server_folder, uidvalidity)) = row else {
|
let Some((server_uid, server_folder)) = row else {
|
||||||
// No IMAP record found, we don't know the UID and folder.
|
// No IMAP record found, we don't know the UID and folder.
|
||||||
return Err(anyhow!("Call download_full() again to try over."));
|
return Err(anyhow!("Call download_full() again to try over."));
|
||||||
};
|
};
|
||||||
|
|
||||||
session
|
session
|
||||||
.fetch_single_msg(
|
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
|
||||||
context,
|
|
||||||
&server_folder,
|
|
||||||
uidvalidity,
|
|
||||||
server_uid,
|
|
||||||
msg.rfc724_mid.clone(),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -194,7 +187,6 @@ impl Session {
|
|||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
folder: &str,
|
folder: &str,
|
||||||
uidvalidity: u32,
|
|
||||||
uid: u32,
|
uid: u32,
|
||||||
rfc724_mid: String,
|
rfc724_mid: String,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -214,16 +206,8 @@ impl Session {
|
|||||||
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
|
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
|
||||||
uid_message_ids.insert(uid, rfc724_mid);
|
uid_message_ids.insert(uid, rfc724_mid);
|
||||||
let (sender, receiver) = async_channel::unbounded();
|
let (sender, receiver) = async_channel::unbounded();
|
||||||
self.fetch_many_msgs(
|
self.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false, sender)
|
||||||
context,
|
.await?;
|
||||||
folder,
|
|
||||||
uidvalidity,
|
|
||||||
vec![uid],
|
|
||||||
&uid_message_ids,
|
|
||||||
false,
|
|
||||||
sender,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if receiver.recv().await.is_err() {
|
if receiver.recv().await.is_err() {
|
||||||
bail!("Failed to fetch UID {uid}");
|
bail!("Failed to fetch UID {uid}");
|
||||||
}
|
}
|
||||||
|
|||||||
99
src/imap.rs
99
src/imap.rs
@@ -620,71 +620,38 @@ impl Imap {
|
|||||||
|
|
||||||
// Determine the target folder where the message should be moved to.
|
// Determine the target folder where the message should be moved to.
|
||||||
//
|
//
|
||||||
// If we have seen the message on the IMAP server before, do not move it.
|
// We only move the messages from the INBOX and Spam folders.
|
||||||
// This is required to avoid infinite MOVE loop on IMAP servers
|
// This is required to avoid infinite MOVE loop on IMAP servers
|
||||||
// that alias `DeltaChat` folder to other names.
|
// that alias `DeltaChat` folder to other names.
|
||||||
// For example, some Dovecot servers alias `DeltaChat` folder to `INBOX.DeltaChat`.
|
// For example, some Dovecot servers alias `DeltaChat` folder to `INBOX.DeltaChat`.
|
||||||
// In this case Delta Chat configured with `DeltaChat` as the destination folder
|
// In this case moving from `INBOX.DeltaChat` to `DeltaChat`
|
||||||
// would detect messages in the `INBOX.DeltaChat` folder
|
// results in the messages getting a new UID,
|
||||||
// and try to move them to the `DeltaChat` folder.
|
// so the messages will be detected as new
|
||||||
// Such move to the same folder results in the messages
|
|
||||||
// getting a new UID, so the messages will be detected as new
|
|
||||||
// in the `INBOX.DeltaChat` folder again.
|
// in the `INBOX.DeltaChat` folder again.
|
||||||
let _target;
|
let delete = if let Some(message_id) = &message_id {
|
||||||
let target = if let Some(message_id) = &message_id {
|
message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
|
||||||
let msg_info =
|
|
||||||
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
|
|
||||||
let delete = if let Some((_, _, true)) = msg_info {
|
|
||||||
info!(context, "Deleting locally deleted message {message_id}.");
|
|
||||||
true
|
|
||||||
} else if let Some((_, ts_sent_old, _)) = msg_info {
|
|
||||||
let is_chat_msg = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
|
||||||
let ts_sent = headers
|
|
||||||
.get_header_value(HeaderDef::Date)
|
|
||||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let is_dup = is_dup_msg(is_chat_msg, ts_sent, ts_sent_old);
|
|
||||||
if is_dup {
|
|
||||||
info!(context, "Deleting duplicate message {message_id}.");
|
|
||||||
}
|
|
||||||
is_dup
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
if delete {
|
|
||||||
&delete_target
|
|
||||||
} else if context
|
|
||||||
.sql
|
|
||||||
.exists(
|
|
||||||
"SELECT COUNT (*) FROM imap WHERE rfc724_mid=?",
|
|
||||||
(message_id,),
|
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
{
|
.is_some_and(|(_msg_id, deleted)| deleted)
|
||||||
info!(
|
|
||||||
context,
|
|
||||||
"Not moving the message {} that we have seen before.", &message_id
|
|
||||||
);
|
|
||||||
folder
|
|
||||||
} else {
|
|
||||||
_target = target_folder(context, folder, folder_meaning, &headers).await?;
|
|
||||||
&_target
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Do not move the messages without Message-ID.
|
false
|
||||||
// We cannot reliably determine if we have seen them before,
|
|
||||||
// so it is safer not to move them.
|
|
||||||
warn!(
|
|
||||||
context,
|
|
||||||
"Not moving the message that does not have a Message-ID."
|
|
||||||
);
|
|
||||||
folder
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a fake Message-ID to identify the message in the database
|
// Generate a fake Message-ID to identify the message in the database
|
||||||
// if the message has no real Message-ID.
|
// if the message has no real Message-ID.
|
||||||
let message_id = message_id.unwrap_or_else(create_message_id);
|
let message_id = message_id.unwrap_or_else(create_message_id);
|
||||||
|
|
||||||
|
if delete {
|
||||||
|
info!(context, "Deleting locally deleted message {message_id}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let _target;
|
||||||
|
let target = if delete {
|
||||||
|
&delete_target
|
||||||
|
} else {
|
||||||
|
_target = target_folder(context, folder, folder_meaning, &headers).await?;
|
||||||
|
&_target
|
||||||
|
};
|
||||||
|
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.execute(
|
.execute(
|
||||||
@@ -768,7 +735,6 @@ impl Imap {
|
|||||||
.fetch_many_msgs(
|
.fetch_many_msgs(
|
||||||
context,
|
context,
|
||||||
folder,
|
folder,
|
||||||
uid_validity,
|
|
||||||
uids_fetch_in_batch.split_off(0),
|
uids_fetch_in_batch.split_off(0),
|
||||||
&uid_message_ids,
|
&uid_message_ids,
|
||||||
fetch_partially,
|
fetch_partially,
|
||||||
@@ -1383,12 +1349,10 @@ impl Session {
|
|||||||
///
|
///
|
||||||
/// If the message is incorrect or there is a failure to write a message to the database,
|
/// If the message is incorrect or there is a failure to write a message to the database,
|
||||||
/// it is skipped and the error is logged.
|
/// it is skipped and the error is logged.
|
||||||
#[expect(clippy::too_many_arguments)]
|
|
||||||
pub(crate) async fn fetch_many_msgs(
|
pub(crate) async fn fetch_many_msgs(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
folder: &str,
|
folder: &str,
|
||||||
uidvalidity: u32,
|
|
||||||
request_uids: Vec<u32>,
|
request_uids: Vec<u32>,
|
||||||
uid_message_ids: &BTreeMap<u32, String>,
|
uid_message_ids: &BTreeMap<u32, String>,
|
||||||
fetch_partially: bool,
|
fetch_partially: bool,
|
||||||
@@ -1514,9 +1478,6 @@ impl Session {
|
|||||||
);
|
);
|
||||||
let res = receive_imf_inner(
|
let res = receive_imf_inner(
|
||||||
context,
|
context,
|
||||||
folder,
|
|
||||||
uidvalidity,
|
|
||||||
request_uid,
|
|
||||||
rfc724_mid,
|
rfc724_mid,
|
||||||
body,
|
body,
|
||||||
is_seen,
|
is_seen,
|
||||||
@@ -1530,9 +1491,6 @@ impl Session {
|
|||||||
}
|
}
|
||||||
receive_imf_inner(
|
receive_imf_inner(
|
||||||
context,
|
context,
|
||||||
folder,
|
|
||||||
uidvalidity,
|
|
||||||
request_uid,
|
|
||||||
rfc724_mid,
|
rfc724_mid,
|
||||||
body,
|
body,
|
||||||
is_seen,
|
is_seen,
|
||||||
@@ -2112,7 +2070,9 @@ pub async fn target_folder_cfg(
|
|||||||
|
|
||||||
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 needs_move_to_mvbox(context, headers).await? {
|
} else if folder_meaning == FolderMeaning::Inbox
|
||||||
|
&& needs_move_to_mvbox(context, headers).await?
|
||||||
|
{
|
||||||
Ok(Some(Config::ConfiguredMvboxFolder))
|
Ok(Some(Config::ConfiguredMvboxFolder))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -2260,7 +2220,9 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
|
|||||||
];
|
];
|
||||||
let lower = folder_name.to_lowercase();
|
let lower = folder_name.to_lowercase();
|
||||||
|
|
||||||
if SENT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
if lower == "inbox" {
|
||||||
|
FolderMeaning::Inbox
|
||||||
|
} else if SENT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||||
FolderMeaning::Sent
|
FolderMeaning::Sent
|
||||||
} else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
} else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
|
||||||
FolderMeaning::Spam
|
FolderMeaning::Spam
|
||||||
@@ -2416,15 +2378,6 @@ pub(crate) async fn prefetch_should_download(
|
|||||||
Ok(should_download)
|
Ok(should_download)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether a message is a duplicate (resent message).
|
|
||||||
pub(crate) fn is_dup_msg(is_chat_msg: bool, ts_sent: i64, ts_sent_old: i64) -> bool {
|
|
||||||
// If the existing message has timestamp_sent == 0, that means we don't know its actual sent
|
|
||||||
// timestamp, so don't delete the new message. E.g. outgoing messages have zero timestamp_sent
|
|
||||||
// because they are stored to the db before sending. Also consider as duplicates only messages
|
|
||||||
// with greater timestamp to avoid deleting both messages in a multi-device setting.
|
|
||||||
is_chat_msg && ts_sent_old != 0 && ts_sent > ts_sent_old
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks messages in `msgs` table as seen, searching for them by UID.
|
/// Marks messages in `msgs` table as seen, searching for them by UID.
|
||||||
///
|
///
|
||||||
/// Returns updated chat ID if any message was marked as seen.
|
/// Returns updated chat ID if any message was marked as seen.
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
|||||||
("Sent", false, false, "Sent"),
|
("Sent", false, false, "Sent"),
|
||||||
("Sent", false, true, "Sent"),
|
("Sent", false, true, "Sent"),
|
||||||
("Sent", true, false, "Sent"),
|
("Sent", true, false, "Sent"),
|
||||||
("Sent", true, true, "DeltaChat"),
|
("Sent", true, true, "Sent"),
|
||||||
("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, 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", 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, 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
|
||||||
@@ -202,7 +202,7 @@ const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
|||||||
("Sent", false, false, "Sent"),
|
("Sent", false, false, "Sent"),
|
||||||
("Sent", false, true, "Sent"),
|
("Sent", false, true, "Sent"),
|
||||||
("Sent", true, false, "Sent"),
|
("Sent", true, false, "Sent"),
|
||||||
("Sent", true, true, "DeltaChat"),
|
("Sent", true, true, "Sent"),
|
||||||
("Spam", false, false, "Spam"),
|
("Spam", false, false, "Spam"),
|
||||||
("Spam", false, true, "INBOX"),
|
("Spam", false, true, "INBOX"),
|
||||||
("Spam", true, false, "Spam"),
|
("Spam", true, false, "Spam"),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl Imap {
|
|||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
// First of all, debounce to once per minute:
|
// First of all, debounce to once per minute:
|
||||||
{
|
{
|
||||||
let mut last_scan = context.last_full_folder_scan.lock().await;
|
let mut last_scan = session.last_full_folder_scan.lock().await;
|
||||||
if let Some(last_scan) = *last_scan {
|
if let Some(last_scan) = *last_scan {
|
||||||
let elapsed_secs = time_elapsed(&last_scan).as_secs();
|
let elapsed_secs = time_elapsed(&last_scan).as_secs();
|
||||||
let debounce_secs = context
|
let debounce_secs = context
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ use anyhow::{Context as _, Result};
|
|||||||
use async_imap::Session as ImapSession;
|
use async_imap::Session as ImapSession;
|
||||||
use async_imap::types::Mailbox;
|
use async_imap::types::Mailbox;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::imap::capabilities::Capabilities;
|
use crate::imap::capabilities::Capabilities;
|
||||||
use crate::net::session::SessionStream;
|
use crate::net::session::SessionStream;
|
||||||
|
use crate::tools;
|
||||||
|
|
||||||
/// Prefetch:
|
/// Prefetch:
|
||||||
/// - Message-ID to check if we already have the message.
|
/// - Message-ID to check if we already have the message.
|
||||||
@@ -40,6 +42,8 @@ pub(crate) struct Session {
|
|||||||
|
|
||||||
pub selected_folder_needs_expunge: bool,
|
pub selected_folder_needs_expunge: bool,
|
||||||
|
|
||||||
|
pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
|
||||||
|
|
||||||
/// True if currently selected folder has new messages.
|
/// True if currently selected folder has new messages.
|
||||||
///
|
///
|
||||||
/// Should be false if no folder is currently selected.
|
/// Should be false if no folder is currently selected.
|
||||||
@@ -71,6 +75,7 @@ impl Session {
|
|||||||
selected_folder: None,
|
selected_folder: None,
|
||||||
selected_mailbox: None,
|
selected_mailbox: None,
|
||||||
selected_folder_needs_expunge: false,
|
selected_folder_needs_expunge: false,
|
||||||
|
last_full_folder_scan: Mutex::new(None),
|
||||||
new_mail: false,
|
new_mail: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1234,7 +1234,7 @@ impl Message {
|
|||||||
/// `References` header is not taken into account.
|
/// `References` header is not taken into account.
|
||||||
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
|
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
|
||||||
if let Some(in_reply_to) = &self.in_reply_to {
|
if let Some(in_reply_to) = &self.in_reply_to {
|
||||||
if let Some((msg_id, _ts_sent)) = rfc724_mid_exists(context, in_reply_to).await? {
|
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||||
let msg = Message::load_from_db_optional(context, msg_id).await?;
|
let msg = Message::load_from_db_optional(context, msg_id).await?;
|
||||||
return Ok(msg);
|
return Ok(msg);
|
||||||
}
|
}
|
||||||
@@ -2043,13 +2043,13 @@ pub async fn estimate_deletion_cnt(
|
|||||||
pub(crate) async fn rfc724_mid_exists(
|
pub(crate) async fn rfc724_mid_exists(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
rfc724_mid: &str,
|
rfc724_mid: &str,
|
||||||
) -> Result<Option<(MsgId, i64)>> {
|
) -> Result<Option<MsgId>> {
|
||||||
Ok(rfc724_mid_exists_ex(context, rfc724_mid, "1")
|
Ok(rfc724_mid_exists_ex(context, rfc724_mid, "1")
|
||||||
.await?
|
.await?
|
||||||
.map(|(id, ts_sent, _)| (id, ts_sent)))
|
.map(|(id, _)| id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns [MsgId] and "sent" timestamp of the most recent message with given `rfc724_mid`
|
/// Returns [MsgId] of the most recent message with given `rfc724_mid`
|
||||||
/// (Message-ID header) and bool `expr` result if such messages exists in the db.
|
/// (Message-ID header) and bool `expr` result if such messages exists in the db.
|
||||||
///
|
///
|
||||||
/// * `expr`: SQL expression additionally passed into `SELECT`. Evaluated to `true` iff it is true
|
/// * `expr`: SQL expression additionally passed into `SELECT`. Evaluated to `true` iff it is true
|
||||||
@@ -2058,7 +2058,7 @@ pub(crate) async fn rfc724_mid_exists_ex(
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
rfc724_mid: &str,
|
rfc724_mid: &str,
|
||||||
expr: &str,
|
expr: &str,
|
||||||
) -> Result<Option<(MsgId, i64, bool)>> {
|
) -> Result<Option<(MsgId, bool)>> {
|
||||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||||
if rfc724_mid.is_empty() {
|
if rfc724_mid.is_empty() {
|
||||||
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
|
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
|
||||||
@@ -2076,9 +2076,8 @@ pub(crate) async fn rfc724_mid_exists_ex(
|
|||||||
(rfc724_mid,),
|
(rfc724_mid,),
|
||||||
|row| {
|
|row| {
|
||||||
let msg_id: MsgId = row.get(0)?;
|
let msg_id: MsgId = row.get(0)?;
|
||||||
let timestamp_sent: i64 = row.get(1)?;
|
|
||||||
let expr_res: bool = row.get(2)?;
|
let expr_res: bool = row.get(2)?;
|
||||||
Ok((msg_id, timestamp_sent, expr_res))
|
Ok((msg_id, expr_res))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -2098,7 +2097,7 @@ pub(crate) async fn get_by_rfc724_mids(
|
|||||||
) -> Result<Option<Message>> {
|
) -> Result<Option<Message>> {
|
||||||
let mut latest = None;
|
let mut latest = None;
|
||||||
for id in mids.iter().rev() {
|
for id in mids.iter().rev() {
|
||||||
let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? else {
|
let Some(msg_id) = rfc724_mid_exists(context, id).await? else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
|
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ pub(crate) async fn set_msg_reaction(
|
|||||||
reaction: Reaction,
|
reaction: Reaction,
|
||||||
is_incoming_fresh: bool,
|
is_incoming_fresh: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
|
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
|
||||||
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
|
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
|
||||||
|
|
||||||
if is_incoming_fresh
|
if is_incoming_fresh
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use regex::Regex;
|
|||||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, remove_from_chat_contacts_table};
|
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, remove_from_chat_contacts_table};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
|
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
|
||||||
use crate::contact::{Contact, ContactId, Origin, mark_contact_id_as_verified};
|
use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::debug_logging::maybe_set_logging_xdc_inner;
|
use crate::debug_logging::maybe_set_logging_xdc_inner;
|
||||||
use crate::download::DownloadState;
|
use crate::download::DownloadState;
|
||||||
@@ -45,7 +45,6 @@ use crate::stock_str;
|
|||||||
use crate::sync::Sync::*;
|
use crate::sync::Sync::*;
|
||||||
use crate::tools::{self, buf_compress, remove_subject_prefix};
|
use crate::tools::{self, buf_compress, remove_subject_prefix};
|
||||||
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
|
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
|
||||||
use crate::{contact, imap};
|
|
||||||
|
|
||||||
/// This is the struct that is returned after receiving one email (aka MIME message).
|
/// This is the struct that is returned after receiving one email (aka MIME message).
|
||||||
///
|
///
|
||||||
@@ -154,8 +153,8 @@ pub async fn receive_imf(
|
|||||||
seen: bool,
|
seen: bool,
|
||||||
) -> Result<Option<ReceivedMsg>> {
|
) -> Result<Option<ReceivedMsg>> {
|
||||||
let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
|
let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
|
||||||
let rfc724_mid =
|
let rfc724_mid = crate::imap::prefetch_get_message_id(&mail.headers)
|
||||||
imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
|
.unwrap_or_else(crate::imap::create_message_id);
|
||||||
if let Some(download_limit) = context.download_limit().await? {
|
if let Some(download_limit) = context.download_limit().await? {
|
||||||
let download_limit: usize = download_limit.try_into()?;
|
let download_limit: usize = download_limit.try_into()?;
|
||||||
if imf_raw.len() > download_limit {
|
if imf_raw.len() > download_limit {
|
||||||
@@ -189,9 +188,6 @@ pub(crate) async fn receive_imf_from_inbox(
|
|||||||
) -> Result<Option<ReceivedMsg>> {
|
) -> Result<Option<ReceivedMsg>> {
|
||||||
receive_imf_inner(
|
receive_imf_inner(
|
||||||
context,
|
context,
|
||||||
"INBOX",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
rfc724_mid,
|
rfc724_mid,
|
||||||
imf_raw,
|
imf_raw,
|
||||||
seen,
|
seen,
|
||||||
@@ -493,12 +489,8 @@ async fn get_to_and_past_contact_ids(
|
|||||||
///
|
///
|
||||||
/// If `partial` is set, it contains the full message size in bytes and an optional error text for
|
/// If `partial` is set, it contains the full message size in bytes and an optional error text for
|
||||||
/// the partially downloaded message.
|
/// the partially downloaded message.
|
||||||
#[expect(clippy::too_many_arguments)]
|
|
||||||
pub(crate) async fn receive_imf_inner(
|
pub(crate) async fn receive_imf_inner(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
folder: &str,
|
|
||||||
uidvalidity: u32,
|
|
||||||
uid: u32,
|
|
||||||
rfc724_mid: &str,
|
rfc724_mid: &str,
|
||||||
imf_raw: &[u8],
|
imf_raw: &[u8],
|
||||||
seen: bool,
|
seen: bool,
|
||||||
@@ -553,7 +545,7 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
// check, if the mail is already in our database.
|
// check, if the mail is already in our database.
|
||||||
// make sure, this check is done eg. before securejoin-processing.
|
// make sure, this check is done eg. before securejoin-processing.
|
||||||
let (replace_msg_id, replace_chat_id);
|
let (replace_msg_id, replace_chat_id);
|
||||||
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||||
replace_msg_id = Some(old_msg_id);
|
replace_msg_id = Some(old_msg_id);
|
||||||
replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
|
replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
|
||||||
.await?
|
.await?
|
||||||
@@ -570,27 +562,8 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
} else {
|
} else {
|
||||||
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
|
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
|
||||||
None
|
None
|
||||||
} else if let Some((old_msg_id, old_ts_sent)) =
|
|
||||||
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
|
|
||||||
{
|
|
||||||
if imap::is_dup_msg(
|
|
||||||
mime_parser.has_chat_version(),
|
|
||||||
mime_parser.timestamp_sent,
|
|
||||||
old_ts_sent,
|
|
||||||
) {
|
|
||||||
info!(context, "Deleting duplicate message {rfc724_mid_orig}.");
|
|
||||||
let target = context.get_delete_msgs_target().await?;
|
|
||||||
context
|
|
||||||
.sql
|
|
||||||
.execute(
|
|
||||||
"UPDATE imap SET target=? WHERE folder=? AND uidvalidity=? AND uid=?",
|
|
||||||
(target, folder, uidvalidity, uid),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Some(old_msg_id)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
|
||||||
};
|
};
|
||||||
replace_chat_id = None;
|
replace_chat_id = None;
|
||||||
}
|
}
|
||||||
@@ -1906,7 +1879,7 @@ async fn add_parts(
|
|||||||
if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
|
if let Some(node_addr) = mime_parser.get_header(HeaderDef::IrohNodeAddr) {
|
||||||
match mime_parser.get_header(HeaderDef::InReplyTo) {
|
match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||||
Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
|
Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
|
||||||
Some((instance_id, _ts_sent)) => {
|
Some(instance_id) => {
|
||||||
if let Err(err) =
|
if let Err(err) =
|
||||||
add_gossip_peer_from_header(context, instance_id, node_addr).await
|
add_gossip_peer_from_header(context, instance_id, node_addr).await
|
||||||
{
|
{
|
||||||
@@ -2220,7 +2193,7 @@ async fn handle_edit_delete(
|
|||||||
from_id: ContactId,
|
from_id: ContactId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
|
if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
|
||||||
if let Some((original_msg_id, _)) = rfc724_mid_exists(context, rfc724_mid).await? {
|
if let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
|
||||||
if let Some(mut original_msg) =
|
if let Some(mut original_msg) =
|
||||||
Message::load_from_db_optional(context, original_msg_id).await?
|
Message::load_from_db_optional(context, original_msg_id).await?
|
||||||
{
|
{
|
||||||
@@ -2261,9 +2234,7 @@ async fn handle_edit_delete(
|
|||||||
|
|
||||||
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
||||||
for rfc724_mid in rfc724_mid_vec {
|
for rfc724_mid in rfc724_mid_vec {
|
||||||
if let Some((msg_id, _)) =
|
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
|
||||||
message::rfc724_mid_exists(context, rfc724_mid).await?
|
|
||||||
{
|
|
||||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||||
if msg.from_id == from_id {
|
if msg.from_id == from_id {
|
||||||
message::delete_msg_locally(context, &msg).await?;
|
message::delete_msg_locally(context, &msg).await?;
|
||||||
@@ -3626,7 +3597,7 @@ async fn get_previous_message(
|
|||||||
) -> Result<Option<Message>> {
|
) -> Result<Option<Message>> {
|
||||||
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
|
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
|
||||||
if let Some(rfc724mid) = parse_message_ids(field).last() {
|
if let Some(rfc724mid) = parse_message_ids(field).last() {
|
||||||
if let Some((msg_id, _)) = rfc724_mid_exists(context, rfc724mid).await? {
|
if let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await? {
|
||||||
return Message::load_from_db_optional(context, msg_id).await;
|
return Message::load_from_db_optional(context, msg_id).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1759,7 +1759,7 @@ async fn check_alias_reply(from_dc: bool, chat_request: bool, group_request: boo
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (msg_id, _) = rfc724_mid_exists(&claire, "non-dc-1@example.org")
|
let msg_id = rfc724_mid_exists(&claire, "non-dc-1@example.org")
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -618,7 +618,7 @@ async fn fetch_idle(
|
|||||||
.await
|
.await
|
||||||
.context("delete_expired_imap_messages")?;
|
.context("delete_expired_imap_messages")?;
|
||||||
} else if folder_config == Config::ConfiguredInboxFolder {
|
} else if folder_config == Config::ConfiguredInboxFolder {
|
||||||
ctx.last_full_folder_scan.lock().await.take();
|
session.last_full_folder_scan.lock().await.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan additional folders only after finishing fetching the watched folder.
|
// Scan additional folders only after finishing fetching the watched folder.
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ impl Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn save_message(&self, src_rfc724_mid: &str, dest_rfc724_mid: &String) -> Result<()> {
|
async fn save_message(&self, src_rfc724_mid: &str, dest_rfc724_mid: &String) -> Result<()> {
|
||||||
if let Some((src_msg_id, _)) = message::rfc724_mid_exists(self, src_rfc724_mid).await? {
|
if let Some(src_msg_id) = message::rfc724_mid_exists(self, src_rfc724_mid).await? {
|
||||||
chat::save_copy_in_self_talk(self, src_msg_id, dest_rfc724_mid).await?;
|
chat::save_copy_in_self_talk(self, src_msg_id, dest_rfc724_mid).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -328,7 +328,7 @@ impl Context {
|
|||||||
let mut modified_chat_ids = HashSet::new();
|
let mut modified_chat_ids = HashSet::new();
|
||||||
let mut msg_ids = Vec::new();
|
let mut msg_ids = Vec::new();
|
||||||
for rfc724_mid in msgs {
|
for rfc724_mid in msgs {
|
||||||
if let Some((msg_id, _)) = message::rfc724_mid_exists(self, rfc724_mid).await? {
|
if let Some(msg_id) = message::rfc724_mid_exists(self, rfc724_mid).await? {
|
||||||
if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
|
if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
|
||||||
message::delete_msg_locally(self, &msg).await?;
|
message::delete_msg_locally(self, &msg).await?;
|
||||||
msg_ids.push(msg.id);
|
msg_ids.push(msg.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user