Add imap table to keep track of message UIDs

`imap` table maps Message-IDs to UIDs on the server. `dc_receive_imf`
no longer gets the UID of the message as an argument and does not
insert the folder and UID of the message into the `msgs`
table. `server_folder` and `server_uid` columns in `msgs` table are
deprecated.

MoveMsg and DeleteMsgOnImap jobs are removed. Now messages are moved
and deleted only in the `fetch_move_delete` procedure that consults
the `target` column of the `imap` table to determine where the message
should go.

Where the message should go is determined after prefetching by the
`imap::target_folder()` procedure.  Messages are only downloaded once
they reach their target folder to avoid race conditions in multidevice
setting, such as:

1. One device trying to FETCH the message while the other tries to
MOVE it.

2. One device marking the message as \Seen in the Inbox while the
other has already copied unseen message to the Movebox and is going to
delete the \Seen message in the Inbox.

3. Device downloads the message from the Inbox while there are newer
messages in the Movebox placed there by the other device, thus
processing the messages out of order.
This commit is contained in:
link2xt
2021-12-19 00:00:00 +00:00
parent 0b810d7d65
commit 12823c2213
22 changed files with 1057 additions and 1329 deletions

View File

@@ -101,7 +101,7 @@ async fn reset_tables(context: &Context, bits: i32) {
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> { async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
let data = dc_read_file(context, filename).await?; let data = dc_read_file(context, filename).await?;
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await { if let Err(err) = dc_receive_imf(context, &data, "import", false).await {
println!("dc_receive_imf errored: {:?}", err); println!("dc_receive_imf errored: {:?}", err);
} }
Ok(()) Ok(())

View File

@@ -878,9 +878,13 @@ class TestOnlineAccount:
acfactory.wait_configure_and_start_io() acfactory.wait_configure_and_start_io()
chat = acfactory.get_accepted_chat(ac1, ac2) chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message1") chat.send_text("message1")
# Message is moved to the movebox
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
# Message is downloaded
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG") ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
def test_move_works_on_self_sent(self, acfactory): def test_move_works_on_self_sent(self, acfactory):
ac1 = acfactory.get_online_configuring_account(move=True) ac1 = acfactory.get_online_configuring_account(move=True)
@@ -1167,6 +1171,9 @@ class TestOnlineAccount:
assert len(msg.chat.get_messages()) == 1 assert len(msg.chat.get_messages()) == 1
ac1.direct_imap.select_config_folder("mvbox")
ac1.direct_imap.idle_start()
lp.sec("ac2: mark incoming message as seen") lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg]) ac2.mark_seen_messages([msg])
@@ -1176,6 +1183,9 @@ class TestOnlineAccount:
assert len(chat.get_messages()) == 1 assert len(chat.get_messages()) == 1
# Wait for the message to be marked as seen on IMAP.
assert ac1.direct_imap.idle_wait_for_seen()
# MDN is received even though MDNs are already disabled # MDN is received even though MDNs are already disabled
assert msg_out.is_out_mdn_received() assert msg_out.is_out_mdn_received()
@@ -2651,11 +2661,7 @@ class TestOnlineAccount:
msg = ac1._evtracker.wait_next_incoming_message() msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "hello" assert msg.text == "hello"
# Wait until the message was moved (if at all) and we are IDLEing again: # The message has been downloaded, which means it has reached its destination.
if inbox_watch == "1":
ac1._evtracker.get_info_contains("INBOX: Idle entering wait-on-remote state")
else:
ac1._evtracker.get_info_contains("IMAP-fake-IDLE: no folder, waiting for interrupt")
ac1.direct_imap.select_folder(expected_destination) ac1.direct_imap.select_folder(expected_destination)
assert len(ac1.direct_imap.get_all_messages()) == 1 assert len(ac1.direct_imap.get_all_messages()) == 1
if folder != expected_destination: if folder != expected_destination:

View File

@@ -4369,7 +4369,7 @@ mod tests {
assert_eq!(msg.match_indices("Gr.").count(), 1); assert_eq!(msg.match_indices("Gr.").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header // Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
dc_receive_imf(&bob, msg.as_bytes(), "INBOX", 1, false) dc_receive_imf(&bob, msg.as_bytes(), "INBOX", false)
.await .await
.unwrap(); .unwrap();
let msg = bob.get_last_msg().await; let msg = bob.get_last_msg().await;
@@ -4389,7 +4389,7 @@ mod tests {
assert_eq!(msg.match_indices("Chat-").count(), 0); assert_eq!(msg.match_indices("Chat-").count(), 0);
// Alice receives this message - she can still detect the group by the `References:`-header // Alice receives this message - she can still detect the group by the `References:`-header
dc_receive_imf(&alice, msg.as_bytes(), "INBOX", 2, false) dc_receive_imf(&alice, msg.as_bytes(), "INBOX", false)
.await .await
.unwrap(); .unwrap();
let msg = alice.get_last_msg().await; let msg = alice.get_last_msg().await;
@@ -4417,7 +4417,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -4466,7 +4465,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -4515,7 +4513,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -4563,7 +4560,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;

View File

@@ -507,7 +507,6 @@ mod tests {
\n\ \n\
hello foo\n", hello foo\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -569,7 +568,6 @@ mod tests {
\n\ \n\
hello foo\n", hello foo\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;

View File

@@ -2084,7 +2084,7 @@ Chat-Version: 1.0
Date: Sun, 22 Mar 2020 22:37:55 +0000 Date: Sun, 22 Mar 2020 22:37:55 +0000
Hi."#; Hi."#;
dc_receive_imf(&alice, mime, "Inbox", 1, false).await?; dc_receive_imf(&alice, mime, "Inbox", false).await?;
let msg = alice.get_last_msg().await; let msg = alice.get_last_msg().await;
let timestamp = msg.get_timestamp(); let timestamp = msg.get_timestamp();

View File

@@ -676,7 +676,7 @@ mod tests {
dc_create_outgoing_rfc724_mid(None, contact.get_addr()) dc_create_outgoing_rfc724_mid(None, contact.get_addr())
); );
println!("{}", msg); println!("{}", msg);
dc_receive_imf(t, msg.as_bytes(), "INBOX", 1, false) dc_receive_imf(t, msg.as_bytes(), "INBOX", false)
.await .await
.unwrap(); .unwrap();
} }

View File

@@ -69,19 +69,9 @@ pub async fn dc_receive_imf(
context: &Context, context: &Context,
imf_raw: &[u8], imf_raw: &[u8],
server_folder: &str, server_folder: &str,
server_uid: u32,
seen: bool, seen: bool,
) -> Result<Option<ReceivedMsg>> { ) -> Result<Option<ReceivedMsg>> {
dc_receive_imf_inner( dc_receive_imf_inner(context, imf_raw, server_folder, seen, None, false).await
context,
imf_raw,
server_folder,
server_uid,
seen,
None,
false,
)
.await
} }
/// If `is_partial_download` is set, it contains the full message size in bytes. /// If `is_partial_download` is set, it contains the full message size in bytes.
@@ -90,14 +80,13 @@ pub(crate) async fn dc_receive_imf_inner(
context: &Context, context: &Context,
imf_raw: &[u8], imf_raw: &[u8],
server_folder: &str, server_folder: &str,
server_uid: u32,
seen: bool, seen: bool,
is_partial_download: Option<u32>, is_partial_download: Option<u32>,
fetching_existing_messages: bool, fetching_existing_messages: bool,
) -> Result<Option<ReceivedMsg>> { ) -> Result<Option<ReceivedMsg>> {
info!( info!(
context, context,
"Receiving message {}/{}, seen={}...", server_folder, server_uid, seen "Receiving message, folder={}, seen={}...", server_folder, seen
); );
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" { if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
@@ -125,16 +114,12 @@ pub(crate) async fn dc_receive_imf_inner(
// client that relies in the SMTP server to generate one. // client that relies in the SMTP server to generate one.
// true eg. for the Webmailer used in all-inkl-KAS // true eg. for the Webmailer used in all-inkl-KAS
dc_create_incoming_rfc724_mid(&mime_parser)); dc_create_incoming_rfc724_mid(&mime_parser));
info!( info!(context, "received message has Message-Id: {}", rfc724_mid);
context,
"received message {} has Message-Id: {}", server_uid, rfc724_mid
);
// 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_partial_download = if let Some((old_server_folder, old_server_uid, old_msg_id)) = let replace_partial_download =
message::rfc724_mid_exists(context, &rfc724_mid).await? if let Some(old_msg_id) = message::rfc724_mid_exists(context, &rfc724_mid).await? {
{
let msg = Message::load_from_db(context, old_msg_id).await?; let msg = Message::load_from_db(context, old_msg_id).await?;
if msg.download_state() != DownloadState::Done && is_partial_download.is_none() { if msg.download_state() != DownloadState::Done && is_partial_download.is_none() {
// the mesage was partially downloaded before and is fully downloaded now. // the mesage was partially downloaded before and is fully downloaded now.
@@ -146,10 +131,7 @@ pub(crate) async fn dc_receive_imf_inner(
true true
} else { } else {
// the message was probably moved around. // the message was probably moved around.
info!(context, "Message already in DB, updating folder/uid."); info!(context, "Message already in DB, doing nothing.");
if old_server_folder != server_folder || old_server_uid != server_uid {
message::update_server_uid(context, &rfc724_mid, server_folder, server_uid).await;
}
return Ok(None); return Ok(None);
} }
} else { } else {
@@ -211,9 +193,8 @@ pub(crate) async fn dc_receive_imf_inner(
incoming, incoming,
incoming_origin, incoming_origin,
server_folder, server_folder,
server_uid,
&to_ids, &to_ids,
rfc724_mid, &rfc724_mid,
sent_timestamp, sent_timestamp,
rcvd_timestamp, rcvd_timestamp,
from_id, from_id,
@@ -327,28 +308,11 @@ pub(crate) async fn dc_receive_imf_inner(
if !created_db_entries.is_empty() { if !created_db_entries.is_empty() {
if needs_delete_job || (delete_server_after == Some(0) && is_partial_download.is_none()) { if needs_delete_job || (delete_server_after == Some(0) && is_partial_download.is_none()) {
for db_entry in &created_db_entries { context
job::add( .sql
context, .execute(
job::Job::new( "UPDATE imap SET target='' WHERE rfc724_mid=?",
Action::DeleteMsgOnImap, paramsv![rfc724_mid],
db_entry.1.to_u32(),
Params::new(),
0,
),
)
.await?;
}
} else if insert_msg_id
.needs_move(context, server_folder.as_ref())
.await
.unwrap_or_default()
.is_some()
{
// Move message if we don't delete it immediately.
job::add(
context,
job::Job::new(Action::MoveMsg, insert_msg_id.to_u32(), Params::new(), 0),
) )
.await?; .await?;
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() { } else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
@@ -442,9 +406,8 @@ async fn add_parts(
incoming: bool, incoming: bool,
incoming_origin: Origin, incoming_origin: Origin,
server_folder: &str, server_folder: &str,
server_uid: u32,
to_ids: &[u32], to_ids: &[u32],
rfc724_mid: String, rfc724_mid: &str,
sent_timestamp: i64, sent_timestamp: i64,
rcvd_timestamp: i64, rcvd_timestamp: i64,
from_id: u32, from_id: u32,
@@ -773,7 +736,6 @@ async fn add_parts(
let is_draft = !context.is_sentbox(server_folder).await? let is_draft = !context.is_sentbox(server_folder).await?
&& mime_parser.get_header(HeaderDef::Received).is_none() && mime_parser.get_header(HeaderDef::Received).is_none()
&& mime_parser.get_header(HeaderDef::ChatVersion).is_none(); && mime_parser.get_header(HeaderDef::ChatVersion).is_none();
// Mozilla Thunderbird does not set \Draft flag on "Templates", but sets // Mozilla Thunderbird does not set \Draft flag on "Templates", but sets
// X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates // X-Mozilla-Draft-Info header, which can be used to detect both drafts and templates
// created by Thunderbird. // created by Thunderbird.
@@ -1110,7 +1072,7 @@ async fn add_parts(
r#" r#"
INSERT INTO msgs INSERT INTO msgs
( (
rfc724_mid, server_folder, server_uid, chat_id, rfc724_mid, chat_id,
from_id, to_id, timestamp, timestamp_sent, from_id, to_id, timestamp, timestamp_sent,
timestamp_rcvd, type, state, msgrmsg, timestamp_rcvd, type, state, msgrmsg,
txt, subject, txt_raw, param, txt, subject, txt_raw, param,
@@ -1124,8 +1086,7 @@ INSERT INTO msgs
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?
?, ?
); );
"#, "#,
)?; )?;
@@ -1162,8 +1123,6 @@ INSERT INTO msgs
stmt.execute(paramsv![ stmt.execute(paramsv![
rfc724_mid, rfc724_mid,
server_folder,
server_uid as i32,
chat_id, chat_id,
if trash { 0 } else { from_id as i32 }, if trash { 0 } else { from_id as i32 },
if trash { 0 } else { to_id as i32 }, if trash { 0 } else { to_id as i32 },
@@ -2177,7 +2136,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 Ok(Some(Message::load_from_db(context, msg_id).await?)); return Ok(Some(Message::load_from_db(context, msg_id).await?));
} }
} }
@@ -2194,7 +2153,7 @@ async fn get_rfc724_mid_in_list(context: &Context, mid_list: &str) -> Result<Opt
} }
for id in parse_message_ids(mid_list).iter().rev() { for id in parse_message_ids(mid_list).iter().rev() {
if let Some((_, _, msg_id)) = rfc724_mid_exists(context, id).await? { if let Some(msg_id) = rfc724_mid_exists(context, id).await? {
let msg = Message::load_from_db(context, msg_id).await?; let msg = Message::load_from_db(context, msg_id).await?;
if msg.chat_id != DC_CHAT_ID_TRASH { if msg.chat_id != DC_CHAT_ID_TRASH {
return Ok(Some(msg)); return Ok(Some(msg));
@@ -2432,21 +2391,17 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); assert_eq!(chats.len(), 0);
dc_receive_imf(&t, MSGRMSG, "INBOX", 1, false) dc_receive_imf(&t, MSGRMSG, "INBOX", false).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, "INBOX", false)
.await .await
.unwrap(); .unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, "INBOX", 1, false) dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
dc_receive_imf(&t, GRP_MAIL, "INBOX", 1, false)
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
} }
@@ -2455,9 +2410,7 @@ mod tests {
async fn test_adhoc_group_show_accepted_contact_unknown() { async fn test_adhoc_group_show_accepted_contact_unknown() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, "INBOX", 1, false) dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
.await
.unwrap();
// adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts // adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
@@ -2469,9 +2422,7 @@ mod tests {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
Contact::create(&t, "Bob", "bob@example.com").await.unwrap(); Contact::create(&t, "Bob", "bob@example.com").await.unwrap();
dc_receive_imf(&t, GRP_MAIL, "INBOX", 1, false) dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
.await
.unwrap();
// adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts // adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts
// (and existent chat is required) // (and existent chat is required)
@@ -2485,9 +2436,7 @@ mod tests {
t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
// accept Bob by accepting a delta-message from Bob // accept Bob by accepting a delta-message from Bob
dc_receive_imf(&t, MSGRMSG, "INBOX", 1, false) dc_receive_imf(&t, MSGRMSG, "INBOX", false).await.unwrap();
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0); let chat_id = chats.get_chat_id(0);
@@ -2508,7 +2457,7 @@ mod tests {
); );
// receive a non-delta-message from Bob, shows up because of the show_emails setting // receive a non-delta-message from Bob, shows up because of the show_emails setting
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, "INBOX", 2, false) dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, "INBOX", false)
.await .await
.unwrap(); .unwrap();
@@ -2521,9 +2470,7 @@ mod tests {
); );
// let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting // let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting
dc_receive_imf(&t, GRP_MAIL, "INBOX", 3, false) dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2); assert_eq!(chats.len(), 2);
let chat_id = chats.get_chat_id(0); let chat_id = chats.get_chat_id(0);
@@ -2537,9 +2484,7 @@ mod tests {
async fn test_adhoc_group_show_all() { async fn test_adhoc_group_show_all() {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
dc_receive_imf(&t, GRP_MAIL, "INBOX", 1, false) dc_receive_imf(&t, GRP_MAIL, "INBOX", false).await.unwrap();
.await
.unwrap();
// adhoc-group with unknown contacts with show_emails=all will show up in a single chat // adhoc-group with unknown contacts with show_emails=all will show up in a single chat
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
@@ -2612,7 +2557,6 @@ mod tests {
) )
.as_bytes(), .as_bytes(),
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -2658,7 +2602,6 @@ mod tests {
) )
.as_bytes(), .as_bytes(),
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -2701,7 +2644,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -2731,7 +2673,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
).await.unwrap(); ).await.unwrap();
assert_eq!( assert_eq!(
@@ -2773,7 +2714,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -2818,7 +2758,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -2939,7 +2878,6 @@ mod tests {
) )
.as_bytes(), .as_bytes(),
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -2953,15 +2891,14 @@ mod tests {
assert!(crate::imap::prefetch_should_download( assert!(crate::imap::prefetch_should_download(
&t, &t,
&headers, &headers,
"some-other-message-id",
std::iter::empty(), std::iter::empty(),
ShowEmails::Off ShowEmails::Off
) )
.await .await
.unwrap()); .unwrap());
dc_receive_imf(&t, raw_ndn, "INBOX", 1, false) dc_receive_imf(&t, raw_ndn, "INBOX", false).await.unwrap();
.await
.unwrap();
let msg = Message::load_from_db(&t, msg_id).await.unwrap(); let msg = Message::load_from_db(&t, msg_id).await.unwrap();
assert_eq!(msg.state, MessageState::OutFailed); assert_eq!(msg.state, MessageState::OutFailed);
@@ -2989,7 +2926,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -2998,7 +2934,7 @@ mod tests {
let msg_id = chats.get_msg_id(0)?.unwrap(); let msg_id = chats.get_msg_id(0)?.unwrap();
let raw = include_bytes!("../test-data/message/gmail_ndn_group.eml"); let raw = include_bytes!("../test-data/message/gmail_ndn_group.eml");
dc_receive_imf(&t, raw, "INBOX", 1, false).await?; dc_receive_imf(&t, raw, "INBOX", false).await?;
let msg = Message::load_from_db(&t, msg_id).await?; let msg = Message::load_from_db(&t, msg_id).await?;
@@ -3025,7 +2961,7 @@ mod tests {
.set_config(Config::ShowEmails, Some("2")) .set_config(Config::ShowEmails, Some("2"))
.await .await
.unwrap(); .unwrap();
dc_receive_imf(context, imf_raw, "INBOX", 0, false) dc_receive_imf(context, imf_raw, "INBOX", false)
.await .await
.unwrap(); .unwrap();
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
@@ -3069,7 +3005,7 @@ mod tests {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
t.ctx.set_config(Config::ShowEmails, Some("2")).await?; t.ctx.set_config(Config::ShowEmails, Some("2")).await?;
dc_receive_imf(&t.ctx, GH_MAILINGLIST, "INBOX", 1, false).await?; dc_receive_imf(&t.ctx, GH_MAILINGLIST, "INBOX", false).await?;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
@@ -3083,7 +3019,7 @@ mod tests {
assert_eq!(chat.name, "deltachat/deltachat-core-rust"); assert_eq!(chat.name, "deltachat/deltachat-core-rust");
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1); assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1);
dc_receive_imf(&t.ctx, GH_MAILINGLIST2, "INBOX", 1, false).await?; dc_receive_imf(&t.ctx, GH_MAILINGLIST2, "INBOX", false).await?;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
assert_eq!(chats.len(), 1); assert_eq!(chats.len(), 1);
@@ -3133,7 +3069,7 @@ mod tests {
.set_config(Config::ShowEmails, Some("2")) .set_config(Config::ShowEmails, Some("2"))
.await .await
.unwrap(); .unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", 1, false) dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", false)
.await .await
.unwrap(); .unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
@@ -3155,7 +3091,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", 1, false) dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", false)
.await .await
.unwrap(); .unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
@@ -3170,7 +3106,7 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); // Test that the message disappeared assert_eq!(chats.len(), 0); // Test that the message disappeared
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", 2, false) dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false)
.await .await
.unwrap(); .unwrap();
@@ -3188,7 +3124,7 @@ mod tests {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
dc_receive_imf(&t, DC_MAILINGLIST, "INBOX", 1000, false) dc_receive_imf(&t, DC_MAILINGLIST, "INBOX", false)
.await .await
.unwrap(); .unwrap();
let blocked = Contact::get_all_blocked(&t).await.unwrap(); let blocked = Contact::get_all_blocked(&t).await.unwrap();
@@ -3210,7 +3146,7 @@ mod tests {
let blocked = Contact::get_all_blocked(&t).await.unwrap(); let blocked = Contact::get_all_blocked(&t).await.unwrap();
assert_eq!(blocked.len(), 0); assert_eq!(blocked.len(), 0);
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", 1001, false) dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false)
.await .await
.unwrap(); .unwrap();
let msg = t.get_last_msg().await; let msg = t.get_last_msg().await;
@@ -3226,7 +3162,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", 1, false) dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", false)
.await .await
.unwrap(); .unwrap();
@@ -3241,7 +3177,7 @@ mod tests {
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap(); let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
assert_eq!(msgs.len(), 1); // ...and contains 1 message assert_eq!(msgs.len(), 1); // ...and contains 1 message
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", 1, false) dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false)
.await .await
.unwrap(); .unwrap();
@@ -3261,7 +3197,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", 1, false) dc_receive_imf(&t.ctx, DC_MAILINGLIST, "INBOX", false)
.await .await
.unwrap(); .unwrap();
@@ -3273,7 +3209,7 @@ mod tests {
assert_eq!(chats.len(), 1); // Test that the message is shown assert_eq!(chats.len(), 1); // Test that the message is shown
assert!(!chat_id.is_special()); assert!(!chat_id.is_special());
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", 1, false) dc_receive_imf(&t.ctx, DC_MAILINGLIST2, "INBOX", false)
.await .await
.unwrap(); .unwrap();
@@ -3299,7 +3235,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3332,7 +3267,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3362,7 +3296,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3387,7 +3320,6 @@ mod tests {
&t, &t,
include_bytes!("../test-data/message/mailinglist_dhl.eml"), include_bytes!("../test-data/message/mailinglist_dhl.eml"),
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3414,7 +3346,6 @@ mod tests {
&t, &t,
include_bytes!("../test-data/message/mailinglist_dpd.eml"), include_bytes!("../test-data/message/mailinglist_dpd.eml"),
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3441,7 +3372,6 @@ mod tests {
&t, &t,
include_bytes!("../test-data/message/mailinglist_xt_local_microsoft.eml"), include_bytes!("../test-data/message/mailinglist_xt_local_microsoft.eml"),
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -3454,7 +3384,6 @@ mod tests {
&t, &t,
include_bytes!("../test-data/message/mailinglist_xt_local_spiegel.eml"), include_bytes!("../test-data/message/mailinglist_xt_local_spiegel.eml"),
"INBOX", "INBOX",
2,
false, false,
) )
.await?; .await?;
@@ -3475,7 +3404,6 @@ mod tests {
&t, &t,
include_bytes!("../test-data/message/mailinglist_xing.eml"), include_bytes!("../test-data/message/mailinglist_xing.eml"),
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -3498,7 +3426,6 @@ mod tests {
&t, &t,
include_bytes!("../test-data/message/mailinglist_ttline.eml"), include_bytes!("../test-data/message/mailinglist_ttline.eml"),
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -3526,7 +3453,6 @@ mod tests {
&t, &t,
include_bytes!("../test-data/message/mailinglist_with_mimepart_footer.eml"), include_bytes!("../test-data/message/mailinglist_with_mimepart_footer.eml"),
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3557,7 +3483,6 @@ mod tests {
&t, &t,
include_bytes!("../test-data/message/mailinglist_with_mimepart_footer_signed.eml"), include_bytes!("../test-data/message/mailinglist_with_mimepart_footer_signed.eml"),
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3615,7 +3540,6 @@ YEAAAAAA!.
) )
.as_bytes(), .as_bytes(),
"Sent", "Sent",
1,
false, false,
) )
.await .await
@@ -3673,7 +3597,6 @@ YEAAAAAA!.
&t, &t,
include_bytes!("../test-data/message/many_images_amazon_via_apple_mail.eml"), include_bytes!("../test-data/message/many_images_amazon_via_apple_mail.eml"),
"INBOX", "INBOX",
0,
false, false,
) )
.await .await
@@ -3707,7 +3630,6 @@ YEAAAAAA!.
\n\ \n\
hello foo\n", hello foo\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3726,7 +3648,6 @@ YEAAAAAA!.
\n\ \n\
reply foo\n", reply foo\n",
"INBOX", "INBOX",
2,
false, false,
) )
.await .await
@@ -3776,7 +3697,6 @@ YEAAAAAA!.
\n\ \n\
hello foo\n", hello foo\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3796,7 +3716,6 @@ YEAAAAAA!.
\n\ \n\
classic reply\n", classic reply\n",
"INBOX", "INBOX",
2,
false, false,
) )
.await .await
@@ -3824,7 +3743,6 @@ YEAAAAAA!.
\n\ \n\
chat reply\n", chat reply\n",
"INBOX", "INBOX",
3,
false, false,
) )
.await .await
@@ -3853,7 +3771,6 @@ YEAAAAAA!.
\n\ \n\
private reply\n", private reply\n",
"INBOX", "INBOX",
4,
false, false,
) )
.await .await
@@ -3968,7 +3885,7 @@ YEAAAAAA!.
.set_config(Config::ShowEmails, Some("2")) .set_config(Config::ShowEmails, Some("2"))
.await .await
.unwrap(); .unwrap();
dc_receive_imf(&alice, claire_request.as_bytes(), "INBOX", 1, false) dc_receive_imf(&alice, claire_request.as_bytes(), "INBOX", false)
.await .await
.unwrap(); .unwrap();
@@ -3994,11 +3911,11 @@ YEAAAAAA!.
.set_config(Config::ShowEmails, Some("2")) .set_config(Config::ShowEmails, Some("2"))
.await .await
.unwrap(); .unwrap();
dc_receive_imf(&claire, claire_request.as_bytes(), "INBOX", 1, false) dc_receive_imf(&claire, claire_request.as_bytes(), "INBOX", false)
.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();
@@ -4030,9 +3947,7 @@ YEAAAAAA!.
// Check that Alice gets the message in the same chat. // Check that Alice gets the message in the same chat.
let request = alice.get_last_msg().await; let request = alice.get_last_msg().await;
dc_receive_imf(&alice, reply, "INBOX", 2, false) dc_receive_imf(&alice, reply, "INBOX", false).await.unwrap();
.await
.unwrap();
let answer = alice.get_last_msg().await; let answer = alice.get_last_msg().await;
assert_eq!(answer.get_subject(), "Re: i have a question"); assert_eq!(answer.get_subject(), "Re: i have a question");
assert!(answer.get_text().unwrap().contains("the version is 1.0")); assert!(answer.get_text().unwrap().contains("the version is 1.0"));
@@ -4055,7 +3970,7 @@ YEAAAAAA!.
// Check that Claire also gets the message in the same chat. // Check that Claire also gets the message in the same chat.
let request = claire.get_last_msg().await; let request = claire.get_last_msg().await;
dc_receive_imf(&claire, reply, "INBOX", 2, false) dc_receive_imf(&claire, reply, "INBOX", false)
.await .await
.unwrap(); .unwrap();
let answer = claire.get_last_msg().await; let answer = claire.get_last_msg().await;
@@ -4126,7 +4041,6 @@ YEAAAAAA!.
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -4154,7 +4068,6 @@ YEAAAAAA!.
\n\ \n\
Reply\n", Reply\n",
"INBOX", "INBOX",
2,
false, false,
) )
.await .await
@@ -4166,16 +4079,9 @@ YEAAAAAA!.
#[async_std::test] #[async_std::test]
async fn test_dont_show_spam() { async fn test_dont_show_spam() {
async fn is_shown( async fn is_shown(t: &TestContext, raw: &[u8], server_folder: &str) -> bool {
t: &TestContext,
raw: &[u8],
server_folder: &str,
server_uid: u32,
) -> bool {
let mail = mailparse::parse_mail(raw).unwrap(); let mail = mailparse::parse_mail(raw).unwrap();
dc_receive_imf(t, raw, server_folder, server_uid, false) dc_receive_imf(t, raw, server_folder, false).await.unwrap();
.await
.unwrap();
t.get_last_msg().await.rfc724_mid t.get_last_msg().await.rfc724_mid
== mail.get_headers().get_first_value("Message-Id").unwrap() == mail.get_headers().get_first_value("Message-Id").unwrap()
} }
@@ -4193,7 +4099,6 @@ YEAAAAAA!.
From: bob@example.org\n\ From: bob@example.org\n\
Chat-Version: 1.0\n", Chat-Version: 1.0\n",
"Inbox", "Inbox",
1
) )
.await, .await,
); );
@@ -4204,7 +4109,6 @@ YEAAAAAA!.
b"Message-Id: abcd2@exmaple.com\n\ b"Message-Id: abcd2@exmaple.com\n\
From: bob@example.org\n", From: bob@example.org\n",
"Inbox", "Inbox",
2
) )
.await, .await,
); );
@@ -4216,7 +4120,6 @@ YEAAAAAA!.
From: bob@example.org\n\ From: bob@example.org\n\
Chat-Version: 1.0\n", Chat-Version: 1.0\n",
"Spam", "Spam",
3
) )
.await, .await,
); );
@@ -4228,7 +4131,6 @@ YEAAAAAA!.
b"Message-Id: abcd4@exmaple.com\n\ b"Message-Id: abcd4@exmaple.com\n\
From: bob@example.org\n", From: bob@example.org\n",
"Spam", "Spam",
4
) )
.await, .await,
); );
@@ -4240,7 +4142,6 @@ YEAAAAAA!.
b"Message-Id: abcd5@exmaple.com\n\ b"Message-Id: abcd5@exmaple.com\n\
From: bob@example.org\n", From: bob@example.org\n",
"Spam", "Spam",
5
) )
.await, .await,
); );
@@ -4265,7 +4166,6 @@ From: <alice@example.org>
Message content", Message content",
"Inbox", "Inbox",
1,
false, false,
) )
.await .await
@@ -4296,7 +4196,6 @@ From: <alice@example.org>
Message content", Message content",
"Sent", "Sent",
1,
false, false,
) )
.await .await
@@ -4345,18 +4244,18 @@ Message content
-- --
Second signature"; Second signature";
dc_receive_imf(&alice, first_message, "Inbox", 1, false).await?; dc_receive_imf(&alice, first_message, "Inbox", false).await?;
let contact = Contact::load_from_db(&alice, bob_contact_id).await?; let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
assert_eq!(contact.get_status(), "First signature"); assert_eq!(contact.get_status(), "First signature");
assert_eq!(contact.get_display_name(), "Bob1"); assert_eq!(contact.get_display_name(), "Bob1");
dc_receive_imf(&alice, second_message, "Inbox", 2, false).await?; dc_receive_imf(&alice, second_message, "Inbox", false).await?;
let contact = Contact::load_from_db(&alice, bob_contact_id).await?; let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
assert_eq!(contact.get_status(), "Second signature"); assert_eq!(contact.get_status(), "Second signature");
assert_eq!(contact.get_display_name(), "Bob2"); assert_eq!(contact.get_display_name(), "Bob2");
// Duplicate message, should be ignored // Duplicate message, should be ignored
dc_receive_imf(&alice, first_message, "Inbox", 3, false).await?; dc_receive_imf(&alice, first_message, "Inbox", false).await?;
// No change because last message is duplicate of the first. // No change because last message is duplicate of the first.
let contact = Contact::load_from_db(&alice, bob_contact_id).await?; let contact = Contact::load_from_db(&alice, bob_contact_id).await?;
@@ -4397,7 +4296,6 @@ Message-ID: <Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de>"
) )
.as_bytes(), .as_bytes(),
"Inbox", "Inbox",
1,
false, false,
) )
.await .await
@@ -4440,7 +4338,6 @@ Private reply"#,
) )
.as_bytes(), .as_bytes(),
"Inbox", "Inbox",
2,
false, false,
) )
.await .await
@@ -4492,7 +4389,6 @@ Message-ID: <Gr.iy1KCE2y65_.mH2TM52miv9@testrun.org>"
) )
.as_bytes(), .as_bytes(),
"Inbox", "Inbox",
1,
false, false,
) )
.await .await
@@ -4540,7 +4436,6 @@ Sent with my Delta Chat Messenger: https://delta.chat
) )
.as_bytes(), .as_bytes(),
"Inbox", "Inbox",
2,
false, false,
) )
.await .await
@@ -4584,7 +4479,6 @@ Message-ID: <Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de>"
) )
.as_bytes(), .as_bytes(),
"Inbox", "Inbox",
1,
false, false,
) )
.await .await
@@ -4624,7 +4518,6 @@ Outgoing reply to all"#,
) )
.as_bytes(), .as_bytes(),
"Inbox", "Inbox",
2,
false, false,
) )
.await .await
@@ -4649,7 +4542,6 @@ In-Reply-To: <outgoing@testrun.org>
Reply to all"#, Reply to all"#,
"Inbox", "Inbox",
3,
false, false,
) )
.await .await
@@ -4692,15 +4584,15 @@ Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Second thread."#; Second thread."#;
// Alice receives two classic emails from Claire. // Alice receives two classic emails from Claire.
dc_receive_imf(&alice, first_thread_mime, "Inbox", 1, false).await?; dc_receive_imf(&alice, first_thread_mime, "Inbox", false).await?;
let alice_first_msg = alice.get_last_msg().await; let alice_first_msg = alice.get_last_msg().await;
dc_receive_imf(&alice, second_thread_mime, "Inbox", 2, false).await?; dc_receive_imf(&alice, second_thread_mime, "Inbox", false).await?;
let alice_second_msg = alice.get_last_msg().await; let alice_second_msg = alice.get_last_msg().await;
// Bob receives the same two emails. // Bob receives the same two emails.
dc_receive_imf(&bob, first_thread_mime, "Inbox", 1, false).await?; dc_receive_imf(&bob, first_thread_mime, "Inbox", false).await?;
let bob_first_msg = bob.get_last_msg().await; let bob_first_msg = bob.get_last_msg().await;
dc_receive_imf(&bob, second_thread_mime, "Inbox", 2, false).await?; dc_receive_imf(&bob, second_thread_mime, "Inbox", false).await?;
let bob_second_msg = bob.get_last_msg().await; let bob_second_msg = bob.get_last_msg().await;
// Messages go to separate chats both for Alice and Bob. // Messages go to separate chats both for Alice and Bob.
@@ -4753,7 +4645,7 @@ Second thread."#;
let mdn_body = rendered_mdn.message; let mdn_body = rendered_mdn.message;
// Alice receives the read receipt. // Alice receives the read receipt.
dc_receive_imf(&alice, &mdn_body, "INBOX", 1, false).await?; dc_receive_imf(&alice, &mdn_body, "INBOX", false).await?;
// Chat should not pop up in the chatlist. // Chat should not pop up in the chatlist.
let chats = Chatlist::try_load(&alice, 0, None, None).await?; let chats = Chatlist::try_load(&alice, 0, None, None).await?;
@@ -4771,7 +4663,6 @@ Second thread."#;
&t, &t,
include_bytes!("../test-data/message/gmx-forward.eml"), include_bytes!("../test-data/message/gmx-forward.eml"),
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -4788,7 +4679,7 @@ Second thread."#;
async fn test_incoming_contact_request() -> Result<()> { async fn test_incoming_contact_request() -> Result<()> {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
dc_receive_imf(&t, MSGRMSG, "INBOX", 1, false).await?; dc_receive_imf(&t, MSGRMSG, "INBOX", false).await?;
let msg = t.get_last_msg().await; let msg = t.get_last_msg().await;
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
assert!(chat.is_contact_request()); assert!(chat.is_contact_request());
@@ -4817,7 +4708,7 @@ From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#; First."#;
dc_receive_imf(&t, mime, "INBOX", 1, false).await?; dc_receive_imf(&t, mime, "INBOX", false).await?;
let first = t.get_last_msg().await; let first = t.get_last_msg().await;
let mime = br#"Subject: Second let mime = br#"Subject: Second
Message-ID: second@example.net Message-ID: second@example.net
@@ -4826,7 +4717,7 @@ From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#; First."#;
dc_receive_imf(&t, mime, "INBOX", 2, false).await?; dc_receive_imf(&t, mime, "INBOX", false).await?;
let second = t.get_last_msg().await; let second = t.get_last_msg().await;
let mime = br#"Subject: Third let mime = br#"Subject: Third
Message-ID: third@example.net Message-ID: third@example.net
@@ -4835,7 +4726,7 @@ From: Bob <bob@example.net>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First."#; First."#;
dc_receive_imf(&t, mime, "INBOX", 3, false).await?; dc_receive_imf(&t, mime, "INBOX", false).await?;
let third = t.get_last_msg().await; let third = t.get_last_msg().await;
let mime = br#"Subject: Message with references. let mime = br#"Subject: Message with references.
@@ -4886,7 +4777,7 @@ Message with references."#;
// Alice sends a message to Bob using Thunderbird. // Alice sends a message to Bob using Thunderbird.
let raw = include_bytes!("../test-data/message/rfc1847_encapsulation.eml"); let raw = include_bytes!("../test-data/message/rfc1847_encapsulation.eml");
dc_receive_imf(&bob, raw, "INBOX", 2, false).await?; dc_receive_imf(&bob, raw, "INBOX", false).await?;
let msg = bob.get_last_msg().await; let msg = bob.get_last_msg().await;
assert!(msg.get_showpadlock()); assert!(msg.get_showpadlock());

View File

@@ -776,7 +776,6 @@ mod tests {
hi hi
Message-ID: 2dfdbde7@example.org Message-ID: 2dfdbde7@example.org
Last seen as: INBOX/1
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000 Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000"; Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
@@ -793,7 +792,6 @@ hi back\r\n\
Sent with my Delta Chat Messenger: https://delta.chat Sent with my Delta Chat Messenger: https://delta.chat
Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
Last seen as: INBOX/1
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000 Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000
Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000 Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
@@ -804,7 +802,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) { async fn check_parse_receive_headers_integration(raw: &[u8], expected: &str) {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
dc_receive_imf(&t, raw, "INBOX", 1, false).await.unwrap(); dc_receive_imf(&t, raw, "INBOX", false).await.unwrap();
let msg = t.get_last_msg().await; let msg = t.get_last_msg().await;
let msg_info = get_msg_info(&t, msg.id).await.unwrap(); let msg_info = get_msg_info(&t, msg.id).await.unwrap();

View File

@@ -129,9 +129,24 @@ impl Job {
} }
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await); let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let server_folder = msg.server_folder.unwrap_or_default(); let row = job_try!(
context
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target!=''",
paramsv![msg.rfc724_mid],
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;
Ok((server_uid, server_folder))
}
)
.await
);
if let Some((server_uid, server_folder)) = row {
match imap match imap
.fetch_single_msg(context, &server_folder, msg.server_uid) .fetch_single_msg(context, &server_folder, server_uid)
.await .await
{ {
ImapActionResult::RetryLater | ImapActionResult::Failed => { ImapActionResult::RetryLater | ImapActionResult::Failed => {
@@ -142,12 +157,21 @@ impl Job {
); );
Status::Finished(Err(anyhow!("Call download_full() again to try over."))) Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
} }
ImapActionResult::Success | ImapActionResult::AlreadyDone => { ImapActionResult::Success => {
// update_download_state() not needed as receive_imf() already // update_download_state() not needed as receive_imf() already
// set the state and emitted the event. // set the state and emitted the event.
Status::Finished(Ok(())) Status::Finished(Ok(()))
} }
} }
} else {
// No IMAP record found, we don't know the UID and folder.
job_try!(
msg.id
.update_download_state(context, DownloadState::Failure)
.await
);
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
}
} }
} }
@@ -172,16 +196,16 @@ impl Imap {
// we are connected, and the folder is selected // we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid); info!(context, "Downloading message {}/{} fully...", folder, uid);
let (_, error_cnt, _) = self let (last_uid, _received) = self
.fetch_many_msgs(context, folder, vec![uid], false, false) .fetch_many_msgs(context, folder, vec![uid], false, false)
.await; .await;
if error_cnt > 0 { if last_uid.is_none() {
return ImapActionResult::Failed; ImapActionResult::Failed
} } else {
ImapActionResult::Success ImapActionResult::Success
} }
} }
}
impl MimeMessage { impl MimeMessage {
/// Creates a placeholder part and add that to `parts`. /// Creates a placeholder part and add that to `parts`.
@@ -309,16 +333,7 @@ mod tests {
Date: Sun, 22 Mar 2020 22:37:57 +0000\ Date: Sun, 22 Mar 2020 22:37:57 +0000\
Content-Type: text/plain"; Content-Type: text/plain";
dc_receive_imf_inner( dc_receive_imf_inner(&t, header.as_bytes(), "INBOX", false, Some(100000), false).await?;
&t,
header.as_bytes(),
"INBOX",
1,
false,
Some(100000),
false,
)
.await?;
let msg = t.get_last_msg().await; let msg = t.get_last_msg().await;
assert_eq!(msg.download_state(), DownloadState::Available); assert_eq!(msg.download_state(), DownloadState::Available);
assert_eq!(msg.get_subject(), "foo"); assert_eq!(msg.get_subject(), "foo");
@@ -331,7 +346,6 @@ mod tests {
&t, &t,
format!("{}\n\n100k text...", header).as_bytes(), format!("{}\n\n100k text...", header).as_bytes(),
"INBOX", "INBOX",
1,
false, false,
None, None,
false, false,
@@ -367,7 +381,6 @@ mod tests {
Date: Sun, 14 Nov 2021 00:10:00 +0000\ Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain", Content-Type: text/plain",
"INBOX", "INBOX",
1,
false, false,
Some(100000), Some(100000),
false, false,

View File

@@ -52,7 +52,7 @@
//! `MsgsChanged` event is emitted when a message deletion is due, to //! `MsgsChanged` event is emitted when a message deletion is due, to
//! make UI reload displayed messages and cause actual deletion. //! make UI reload displayed messages and cause actual deletion.
//! //!
//! Server deletion happens by generating IMAP deletion jobs based on //! Server deletion happens by updating the `imap` table based on
//! the database entries which are expired either according to their //! the database entries which are expired either according to their
//! ephemeral message timers or global `delete_server_after` setting. //! ephemeral message timers or global `delete_server_after` setting.
@@ -73,7 +73,6 @@ use crate::context::Context;
use crate::dc_tools::time; use crate::dc_tools::time;
use crate::download::MIN_DELETE_SERVER_AFTER; use crate::download::MIN_DELETE_SERVER_AFTER;
use crate::events::EventType; use crate::events::EventType;
use crate::job;
use crate::message::{Message, MessageState, MsgId}; use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage; use crate::mimeparser::SystemMessage;
use crate::stock_str; use crate::stock_str;
@@ -263,7 +262,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
impl MsgId { impl MsgId {
/// Returns ephemeral message timer value for the message. /// Returns ephemeral message timer value for the message.
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> { pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
let res = match context let res = match context
.sql .sql
.query_get_value( .query_get_value(
@@ -279,7 +278,7 @@ impl MsgId {
} }
/// Starts ephemeral message timer for the message if it is not started yet. /// Starts ephemeral message timer for the message if it is not started yet.
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> { pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> Result<()> {
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? { if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
let ephemeral_timestamp = time().saturating_add(duration.into()); let ephemeral_timestamp = time().saturating_add(duration.into());
@@ -434,11 +433,8 @@ pub async fn schedule_ephemeral_task(context: &Context) {
} }
} }
/// Returns ID of any expired message that should be deleted from the server. /// Schedules expired IMAP messages for deletion.
/// pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
/// It looks up the trash chat too, to find messages that are already
/// deleted locally, but not deleted on the server.
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result<Option<MsgId>> {
let now = time(); let now = time();
let (threshold_timestamp, threshold_timestamp_extended) = let (threshold_timestamp, threshold_timestamp_extended) =
@@ -452,27 +448,21 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Resul
context context
.sql .sql
.query_row_optional( .execute(
"SELECT id FROM msgs \ "UPDATE imap
WHERE ( \ SET target=''
((download_state = 0 AND timestamp < ?) OR (download_state != 0 AND timestamp < ?)) \ WHERE EXISTS (
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \ SELECT * FROM msgs
) \ WHERE rfc724_mid=imap.rfc724_mid
AND server_uid != 0 \ AND ((download_state = 0 AND timestamp < ?) OR
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?) (download_state != 0 AND timestamp < ?) OR
LIMIT 1", (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
paramsv![ )",
threshold_timestamp, paramsv![threshold_timestamp, threshold_timestamp_extended, now],
threshold_timestamp_extended,
now,
job::Action::DeleteMsgOnImap
],
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
) )
.await .await?;
Ok(())
} }
/// Start ephemeral timers for seen messages if they are not started /// Start ephemeral timers for seen messages if they are not started
@@ -507,9 +497,6 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::param::Params;
use async_std::task::sleep;
use super::*; use super::*;
use crate::config::Config; use crate::config::Config;
use crate::dc_receive_imf::dc_receive_imf; use crate::dc_receive_imf::dc_receive_imf;
@@ -725,7 +712,7 @@ mod tests {
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the /// Test that Alice replying to the chat without a timer at the same time as Bob enables the
/// timer does not result in disabling the timer on the Bob's side. /// timer does not result in disabling the timer on the Bob's side.
#[async_std::test] #[async_std::test]
async fn test_ephemeral_timer_rollback() -> anyhow::Result<()> { async fn test_ephemeral_timer_rollback() -> Result<()> {
let alice = TestContext::new_alice().await; let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await; let bob = TestContext::new_bob().await;
@@ -799,14 +786,14 @@ mod tests {
} }
#[async_std::test] #[async_std::test]
async fn test_ephemeral_delete_msgs() { async fn test_ephemeral_delete_msgs() -> Result<()> {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
let chat = t.get_self_chat().await; let chat = t.get_self_chat().await;
t.send_text(chat.id, "Saved message, which we delete manually") t.send_text(chat.id, "Saved message, which we delete manually")
.await; .await;
let msg = t.get_last_msg_in(chat.id).await; let msg = t.get_last_msg_in(chat.id).await;
msg.id.delete_from_db(&t).await.unwrap(); msg.id.delete_from_db(&t).await?;
check_msg_was_deleted(&t, &chat, msg.id).await; check_msg_was_deleted(&t, &chat, msg.id).await;
chat.id chat.id
@@ -817,36 +804,12 @@ mod tests {
.send_text(chat.id, "Saved message, disappearing after 1s") .send_text(chat.id, "Saved message, disappearing after 1s")
.await; .await;
sleep(Duration::from_millis(1100)).await; async_std::task::sleep(Duration::from_millis(1100)).await;
// Check checks that the msg was deleted locally // Check that the msg was deleted locally.
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await; check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
// Check that the msg will be deleted on the server Ok(())
// First of all, set a server_uid so that DC thinks that it's actually possible to delete
t.sql
.execute(
"UPDATE msgs SET server_uid=1 WHERE id=?",
paramsv![msg.sender_msg_id],
)
.await
.unwrap();
let job = job::load_imap_deletion_job(&t).await.unwrap();
assert_eq!(
job,
Some(job::Job::new(
job::Action::DeleteMsgOnImap,
msg.sender_msg_id.to_u32(),
Params::new(),
0,
))
);
// Let's assume that executing the job fails on first try and the job is saved to the db
job.unwrap().save(&t).await.unwrap();
// Make sure that we don't get yet another job when loading from db
let job2 = job::load_imap_deletion_job(&t).await.unwrap();
assert_eq!(job2, None);
} }
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) { async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
@@ -874,7 +837,7 @@ mod tests {
} }
#[async_std::test] #[async_std::test]
async fn test_load_imap_deletion_msgid() -> Result<()> { async fn test_delete_expired_imap_messages() -> Result<()> {
let t = TestContext::new_alice().await; let t = TestContext::new_alice().await;
const HOUR: i64 = 60 * 60; const HOUR: i64 = 60 * 60;
let now = time(); let now = time();
@@ -887,42 +850,98 @@ mod tests {
(2000, now - 18 * HOUR, now - HOUR), (2000, now - 18 * HOUR, now - HOUR),
(2020, now - 17 * HOUR, now + HOUR), (2020, now - 17 * HOUR, now + HOUR),
] { ] {
let message_id = id.to_string();
t.sql t.sql
.execute( .execute(
"INSERT INTO msgs (id, server_uid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);", "INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
paramsv![id, id, timestamp, ephemeral_timestamp], paramsv![id, message_id, timestamp, ephemeral_timestamp],
)
.await?;
t.sql
.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
paramsv![message_id, id],
) )
.await?; .await?;
} }
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(2000))); async fn test_marked_for_deletion(context: &Context, id: u32) -> Result<()> {
assert_eq!(
context
.sql
.count(
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
paramsv![id.to_string()],
)
.await?,
1
);
Ok(())
}
MsgId::new(2000).delete_from_db(&t).await?; async fn remove_uid(context: &Context, id: u32) -> Result<()> {
assert_eq!(load_imap_deletion_msgid(&t).await?, None); context
.sql
.execute(
"DELETE FROM imap WHERE rfc724_mid=?",
paramsv![id.to_string()],
)
.await?;
Ok(())
}
// This should mark message 2000 for deletion.
delete_expired_imap_messages(&t).await?;
test_marked_for_deletion(&t, 2000).await?;
remove_uid(&t, 2000).await?;
// No other messages are marked for deletion.
assert_eq!(
t.sql
.count("SELECT COUNT(*) FROM imap WHERE target=''", paramsv![],)
.await?,
0
);
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string())) t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
.await?; .await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000))); delete_expired_imap_messages(&t).await?;
test_marked_for_deletion(&t, 1000).await?;
MsgId::new(1000) MsgId::new(1000)
.update_download_state(&t, DownloadState::Available) .update_download_state(&t, DownloadState::Available)
.await?; .await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000))); // delete downloadable anyway t.sql
.execute(
MsgId::new(1000).delete_from_db(&t).await?; "UPDATE imap SET target=folder WHERE rfc724_mid='1000'",
assert_eq!(load_imap_deletion_msgid(&t).await?, None); paramsv![],
)
.await?;
delete_expired_imap_messages(&t).await?;
test_marked_for_deletion(&t, 1000).await?; // Delete downloadable anyway.
remove_uid(&t, 1000).await?;
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string())) t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
.await?; .await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1010))); delete_expired_imap_messages(&t).await?;
test_marked_for_deletion(&t, 1010).await?;
t.sql
.execute(
"UPDATE imap SET target=folder WHERE rfc724_mid='1010'",
paramsv![],
)
.await?;
MsgId::new(1010) MsgId::new(1010)
.update_download_state(&t, DownloadState::Available) .update_download_state(&t, DownloadState::Available)
.await?; .await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, None); // keep downloadable for now delete_expired_imap_messages(&t).await?;
// Keep downloadable for now.
MsgId::new(1010).delete_from_db(&t).await?; assert_eq!(
assert_eq!(load_imap_deletion_msgid(&t).await?, None); t.sql
.count("SELECT COUNT(*) FROM imap WHERE target=''", paramsv![],)
.await?,
0
);
Ok(()) Ok(())
} }
@@ -944,7 +963,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -966,7 +984,6 @@ mod tests {
\n\ \n\
second message\n", second message\n",
"INBOX", "INBOX",
2,
false, false,
) )
.await?; .await?;
@@ -1004,7 +1021,6 @@ mod tests {
\n\ \n\
> hello\n", > hello\n",
"INBOX", "INBOX",
3,
false, false,
) )
.await?; .await?;

View File

@@ -440,9 +440,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.create_chat_with_contact("", "sender@testrun.org") .create_chat_with_contact("", "sender@testrun.org")
.await; .await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml"); let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, "INBOX", 1, false) dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await; let msg = alice.get_last_msg_in(chat.get_id()).await;
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF); assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::No); assert_eq!(msg.is_dc_message, MessengerMessage::No);
@@ -491,9 +489,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.create_chat_with_contact("", "sender@testrun.org") .create_chat_with_contact("", "sender@testrun.org")
.await; .await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml"); let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, "INBOX", 1, false) dc_receive_imf(&alice, raw, "INBOX", false).await.unwrap();
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await; let msg = alice.get_last_msg_in(chat.get_id()).await;
// forward the message to saved-messages, // forward the message to saved-messages,
@@ -560,7 +556,6 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
&t, &t,
include_bytes!("../test-data/message/cp1252-html.eml"), include_bytes!("../test-data/message/cp1252-html.eml"),
"INBOX", "INBOX",
0,
false, false,
) )
.await?; .await?;

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ use async_std::prelude::*;
use super::{get_folder_meaning, get_folder_meaning_by_name}; use super::{get_folder_meaning, get_folder_meaning_by_name};
impl Imap { impl Imap {
pub async fn scan_folders(&mut self, context: &Context) -> Result<()> { pub(crate) async fn scan_folders(&mut self, context: &Context) -> Result<()> {
// 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 = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan { if let Some(last_scan) = *last_scan {
@@ -74,7 +74,7 @@ impl Imap {
self.server_sent_unsolicited_exists(context); self.server_sent_unsolicited_exists(context);
loop { loop {
self.fetch_new_messages(context, folder.name(), false) self.fetch_move_delete(context, folder.name())
.await .await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder"); .ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");

View File

@@ -16,7 +16,6 @@ use crate::config::Config;
use crate::contact::{normalize_name, Contact, Modifier, Origin}; use crate::contact::{normalize_name, Contact, Modifier, Origin};
use crate::context::Context; use crate::context::Context;
use crate::dc_tools::{dc_delete_file, dc_read_file, time}; use crate::dc_tools::{dc_delete_file, dc_read_file, time};
use crate::ephemeral::load_imap_deletion_msgid;
use crate::events::EventType; use crate::events::EventType;
use crate::imap::{Imap, ImapActionResult}; use crate::imap::{Imap, ImapActionResult};
use crate::location; use crate::location;
@@ -96,11 +95,6 @@ pub enum Action {
// this is user initiated so it should have a fairly high priority // this is user initiated so it should have a fairly high priority
UpdateRecentQuota = 140, UpdateRecentQuota = 140,
// Moving message is prioritized lower than deletion so we don't
// bother moving message if it is already scheduled for deletion.
MoveMsg = 200,
DeleteMsgOnImap = 210,
// This job will download partially downloaded messages completely // This job will download partially downloaded messages completely
// and is added when download_full() is called. // and is added when download_full() is called.
// Most messages are downloaded automatically on fetch // Most messages are downloaded automatically on fetch
@@ -133,10 +127,8 @@ impl From<Action> for Thread {
Housekeeping => Thread::Imap, Housekeeping => Thread::Imap,
FetchExistingMsgs => Thread::Imap, FetchExistingMsgs => Thread::Imap,
DeleteMsgOnImap => Thread::Imap,
ResyncFolders => Thread::Imap, ResyncFolders => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap, MarkseenMsgOnImap => Thread::Imap,
MoveMsg => Thread::Imap,
UpdateRecentQuota => Thread::Imap, UpdateRecentQuota => Thread::Imap,
DownloadMsg => Thread::Imap, DownloadMsg => Thread::Imap,
@@ -553,149 +545,6 @@ impl Job {
.await .await
} }
async fn move_msg(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let server_folder = &job_try!(msg
.server_folder
.context("Can't move message out of folder if we don't know the current folder"));
let move_res = msg.id.needs_move(context, server_folder).await;
let dest_folder = match move_res {
Err(e) => {
warn!(context, "could not load dest folder: {}", e);
return Status::RetryLater;
}
Ok(None) => {
warn!(
context,
"msg {} does not need to be moved from {}", msg.id, server_folder
);
return Status::Finished(Ok(()));
}
Ok(Some(config)) => match context.get_config(config).await {
Ok(folder) => folder,
Err(err) => {
warn!(context, "failed to load config: {}", err);
return Status::RetryLater;
}
},
};
if let Some(dest_folder) = dest_folder {
match imap
.mv(context, server_folder, msg.server_uid, &dest_folder)
.await
{
ImapActionResult::RetryLater => Status::RetryLater,
ImapActionResult::Success => {
// Rust-Imap provides no target uid on mv, so just set it to 0, update again when precheck_imf() is called for the moved message
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0).await;
Status::Finished(Ok(()))
}
ImapActionResult::Failed => {
Status::Finished(Err(format_err!("IMAP action failed")))
}
ImapActionResult::AlreadyDone => Status::Finished(Ok(())),
}
} else {
Status::Finished(Err(format_err!("No mvbox folder configured")))
}
}
/// Deletes a message on the server.
///
/// `foreign_id` is a MsgId.
///
/// If the message is in the trash chat or hidden, this job
/// removes database record, otherwise it only clears the
/// `server_uid` column. If there are no more records pointing to
/// the same message on the server, the job actually removes the
/// message on the server.
async fn delete_msg_on_imap(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
if !msg.rfc724_mid.is_empty() {
let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid).await;
info!(
context,
"Running delete job for message {} which has {} entries in the database",
&msg.rfc724_mid,
cnt
);
if cnt > 1 {
info!(
context,
"The message is deleted from the server when all parts are deleted.",
);
} else if cnt == 0 {
warn!(
context,
"The message {} has no UID on the server to delete", &msg.rfc724_mid
);
} else {
/* if this is the last existing part of the message,
we delete the message from the server */
let mid = msg.rfc724_mid;
let server_folder = msg.server_folder.as_ref().unwrap();
let res = if msg.server_uid == 0 {
// Message is already deleted on IMAP server.
ImapActionResult::AlreadyDone
} else {
imap.delete_msg(context, &mid, server_folder, msg.server_uid)
.await
};
match res {
ImapActionResult::AlreadyDone | ImapActionResult::Success => {}
ImapActionResult::RetryLater | ImapActionResult::Failed => {
// If job has failed, for example due to some
// IMAP bug, we postpone it instead of failing
// immediately. This will prevent adding it
// immediately again if user has enabled
// automatic message deletion. Without this,
// we might waste a lot of traffic constantly
// retrying message deletion.
return Status::RetryLater;
}
}
}
if msg.chat_id.is_trash() || msg.hidden {
// Messages are stored in trash chat only to keep
// their server UID and Message-ID. Once message is
// deleted from the server, database record can be
// removed as well.
//
// Hidden messages are similar to trashed, but are
// related to some chat. We also delete their
// database records.
job_try!(msg.id.delete_from_db(context).await)
} else {
// Remove server UID from the database record.
//
// We have either just removed the message from the
// server, in which case UID is not valid anymore, or
// we have more refernces to the same server UID, so
// we remove UID to reduce the number of messages
// pointing to the corresponding UID. Once the counter
// reaches zero, we will remove the message.
job_try!(msg.id.unlink(context).await);
}
Status::Finished(Ok(()))
} else {
/* eg. device messages have no Message-ID */
Status::Finished(Ok(()))
}
}
/// Read the recipients from old emails sent by the user and add them as contacts. /// Read the recipients from old emails sent by the user and add them as contacts.
/// This way, we can already offer them some email addresses they can write to. /// This way, we can already offer them some email addresses they can write to.
/// ///
@@ -774,44 +623,48 @@ impl Job {
} }
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await); let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let row = job_try!(
let folder = msg.server_folder.as_ref().unwrap(); context
.sql
let result = if msg.server_uid == 0 { .query_row_optional(
// The message is moved or deleted by us. "SELECT uid, folder FROM imap
// WHERE rfc724_mid=? AND folder=target
// Do not call set_seen with zero UID, as it will return ORDER BY uid ASC
// ImapActionResult::RetryLater, but we do not want to LIMIT 1",
// retry. If the message was moved, we will create another paramsv![msg.rfc724_mid],
// job to mark the message as seen later. If it was |row| {
// deleted, there is nothing to do. let uid: u32 = row.get(0)?;
info!(context, "Can't mark message as seen: No UID"); let folder: String = row.get(1)?;
ImapActionResult::Failed Ok((uid, folder))
} else { }
imap.set_seen(context, folder, msg.server_uid).await )
}; .await
);
if let Some((server_uid, server_folder)) = row {
let result = imap.set_seen(context, &server_folder, server_uid).await;
match result { match result {
ImapActionResult::RetryLater => Status::RetryLater, ImapActionResult::RetryLater => return Status::RetryLater,
ImapActionResult::AlreadyDone => Status::Finished(Ok(())), ImapActionResult::Success | ImapActionResult::Failed => {}
ImapActionResult::Success | ImapActionResult::Failed => { }
// XXX the message might just have been moved } else {
// we want to send out an MDN anyway info!(
// The job will not be retried so locally context,
"Can't mark the message {} as seen on IMAP because there is no known UID",
msg.rfc724_mid
);
}
// XXX we send MDN even in case of failure to mark the messages as seen, e.g. if it was
// already deleted on the server by another device. The job will not be retried so locally
// there is no risk of double-sending MDNs. // there is no risk of double-sending MDNs.
// //
// Read receipts for system messages are never // Read receipts for system messages are never sent. These messages have no place to
// sent. These messages have no place to display // display received read receipt anyway. And since their text is locally generated,
// received read receipt anyway. And since their text // quoting them is dangerous as it may contain contact names. E.g., for original message
// is locally generated, quoting them is dangerous as // "Group left by me", a read receipt will quote "Group left by <name>", and the name can
// it may contain contact names. E.g., for original // be a display name stored in address book rather than the name sent in the From field by
// message "Group left by me", a read receipt will // the user.
// quote "Group left by <name>", and the name can be a if msg.param.get_bool(Param::WantsMdn).unwrap_or_default() && !msg.is_system_message() {
// display name stored in address book rather than
// the name sent in the From field by the user.
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
&& !msg.is_system_message()
{
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await); let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
if mdns_enabled { if mdns_enabled {
if let Err(err) = send_mdn(context, &msg).await { if let Err(err) = send_mdn(context, &msg).await {
@@ -820,11 +673,10 @@ impl Job {
} }
} }
} }
Status::Finished(Ok(())) Status::Finished(Ok(()))
} }
} }
}
}
/// Delete all pending jobs with the given action. /// Delete all pending jobs with the given action.
pub async fn kill_action(context: &Context, action: Action) -> Result<()> { pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
@@ -1046,13 +898,6 @@ pub(crate) enum Connection<'a> {
Smtp(&'a mut Smtp), Smtp(&'a mut Smtp),
} }
pub(crate) async fn load_imap_deletion_job(context: &Context) -> Result<Option<Job>> {
let res = load_imap_deletion_msgid(context)
.await?
.map(|msg_id| Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0));
Ok(res)
}
impl<'a> fmt::Display for Connection<'a> { impl<'a> fmt::Display for Connection<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@@ -1161,10 +1006,8 @@ async fn perform_job_action(
Action::MaybeSendLocationsEnded => { Action::MaybeSendLocationsEnded => {
location::job_maybe_send_locations_ended(context, job).await location::job_maybe_send_locations_ended(context, job).await
} }
Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await, Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await, Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await, Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
Action::Housekeeping => { Action::Housekeeping => {
sql::housekeeping(context).await.ok_or_log(context); sql::housekeeping(context).await.ok_or_log(context);
@@ -1241,11 +1084,9 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
match action { match action {
Action::Unknown => unreachable!(), Action::Unknown => unreachable!(),
Action::Housekeeping Action::Housekeeping
| Action::DeleteMsgOnImap
| Action::ResyncFolders | Action::ResyncFolders
| Action::MarkseenMsgOnImap | Action::MarkseenMsgOnImap
| Action::FetchExistingMsgs | Action::FetchExistingMsgs
| Action::MoveMsg
| Action::UpdateRecentQuota | Action::UpdateRecentQuota
| Action::DownloadMsg => { | Action::DownloadMsg => {
info!(context, "interrupt: imap"); info!(context, "interrupt: imap");
@@ -1378,12 +1219,6 @@ LIMIT 1;
} }
Thread::Imap => { Thread::Imap => {
if let Some(job) = job { if let Some(job) = job {
if job.action < Action::DeleteMsgOnImap {
Ok(load_imap_deletion_job(context).await?.or(Some(job)))
} else {
Ok(Some(job))
}
} else if let Some(job) = load_imap_deletion_job(context).await? {
Ok(Some(job)) Ok(Some(job))
} else { } else {
Ok(load_housekeeping_job(context).await?) Ok(load_housekeeping_job(context).await?)
@@ -1409,8 +1244,12 @@ mod tests {
VALUES (?, ?, ?, ?, ?, ?);", VALUES (?, ?, ?, ?, ?, ?);",
paramsv![ paramsv![
now, now,
Thread::from(Action::MoveMsg), Thread::from(Action::DownloadMsg),
if valid { Action::MoveMsg as i32 } else { -1 }, if valid {
Action::DownloadMsg as i32
} else {
-1
},
foreign_id, foreign_id,
Params::new().to_string(), Params::new().to_string(),
now now
@@ -1429,7 +1268,7 @@ mod tests {
insert_job(&t, 1, false).await; // This can not be loaded into Job struct. insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
let jobs = load_next( let jobs = load_next(
&t, &t,
Thread::from(Action::MoveMsg), Thread::from(Action::DownloadMsg),
&InterruptInfo::new(false, None), &InterruptInfo::new(false, None),
) )
.await?; .await?;
@@ -1439,7 +1278,7 @@ mod tests {
insert_job(&t, 1, true).await; insert_job(&t, 1, true).await;
let jobs = load_next( let jobs = load_next(
&t, &t,
Thread::from(Action::MoveMsg), Thread::from(Action::DownloadMsg),
&InterruptInfo::new(false, None), &InterruptInfo::new(false, None),
) )
.await?; .await?;
@@ -1455,7 +1294,7 @@ mod tests {
let jobs = load_next( let jobs = load_next(
&t, &t,
Thread::from(Action::MoveMsg), Thread::from(Action::DownloadMsg),
&InterruptInfo::new(false, None), &InterruptInfo::new(false, None),
) )
.await?; .await?;

View File

@@ -10,7 +10,6 @@ use rusqlite::types::ValueRef;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId}; use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::{ use crate::constants::{
Blocked, Chattype, VideochatType, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO, Blocked, Chattype, VideochatType, Viewtype, DC_CHAT_ID_TRASH, DC_CONTACT_ID_INFO,
DC_CONTACT_ID_SELF, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
@@ -29,6 +28,7 @@ use crate::log::LogExt;
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage}; use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
use crate::param::{Param, Params}; use crate::param::{Param, Params};
use crate::pgp::split_armored_data; use crate::pgp::split_armored_data;
use crate::scheduler::InterruptInfo;
use crate::stock_str; use crate::stock_str;
use crate::summary::Summary; use crate::summary::Summary;
@@ -83,65 +83,6 @@ impl MsgId {
Ok(result) Ok(result)
} }
/// Returns Some if the message needs to be moved from `folder`.
/// If yes, returns `ConfiguredInboxFolder`, `ConfiguredMvboxFolder` or `ConfiguredSentboxFolder`,
/// depending on where the message should be moved
pub async fn needs_move(self, context: &Context, folder: &str) -> Result<Option<Config>> {
use Config::*;
if context.is_mvbox(folder).await? {
return Ok(None);
}
let msg = Message::load_from_db(context, self).await?;
if context.is_spam_folder(folder).await? {
let msg_unblocked = msg.chat_id != DC_CHAT_ID_TRASH && msg.chat_blocked == Blocked::Not;
return if msg_unblocked {
if self.needs_move_to_mvbox(context, &msg).await? {
Ok(Some(ConfiguredMvboxFolder))
} else {
Ok(Some(ConfiguredInboxFolder))
}
} else {
// Blocked or contact request message in the spam folder, leave it there
Ok(None)
};
}
if self.needs_move_to_mvbox(context, &msg).await? {
Ok(Some(ConfiguredMvboxFolder))
} else if msg.state.is_outgoing()
&& msg.is_dc_message == MessengerMessage::Yes
&& !msg.is_setupmessage()
&& msg.to_id != DC_CONTACT_ID_SELF // Leave self-chat-messages in the inbox, not sure about this
&& context.is_inbox(folder).await?
&& context.get_config_bool(SentboxMove).await?
&& context.get_config(ConfiguredSentboxFolder).await?.is_some()
{
Ok(Some(ConfiguredSentboxFolder))
} else {
Ok(None)
}
}
async fn needs_move_to_mvbox(self, context: &Context, msg: &Message) -> Result<bool> {
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
}
if msg.is_setupmessage() {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
return Ok(false);
}
match msg.is_dc_message {
MessengerMessage::No => Ok(false),
MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
}
}
/// Put message into trash chat and delete message text. /// Put message into trash chat and delete message text.
/// ///
/// It means the message is deleted locally, but not on the server. /// It means the message is deleted locally, but not on the server.
@@ -187,24 +128,6 @@ WHERE id=?;
Ok(()) Ok(())
} }
/// Removes IMAP server UID and folder from the database record.
///
/// It is used to avoid trying to remove the message from the
/// server multiple times when there are multiple message records
/// pointing to the same server UID.
pub(crate) async fn unlink(self, context: &Context) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs \
SET server_folder='', server_uid=0 \
WHERE id=?",
paramsv![self],
)
.await?;
Ok(())
}
/// Bad evil escape hatch. /// Bad evil escape hatch.
/// ///
/// Avoid using this, eventually types should be cleaned up enough /// Avoid using this, eventually types should be cleaned up enough
@@ -308,8 +231,6 @@ pub struct Message {
pub(crate) subject: String, pub(crate) subject: String,
pub(crate) rfc724_mid: String, pub(crate) rfc724_mid: String,
pub(crate) in_reply_to: Option<String>, pub(crate) in_reply_to: Option<String>,
pub(crate) server_folder: Option<String>,
pub(crate) server_uid: u32,
pub(crate) is_dc_message: MessengerMessage, pub(crate) is_dc_message: MessengerMessage,
pub(crate) mime_modified: bool, pub(crate) mime_modified: bool,
pub(crate) chat_blocked: Blocked, pub(crate) chat_blocked: Blocked,
@@ -329,7 +250,7 @@ impl Message {
pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> { pub async fn load_from_db(context: &Context, id: MsgId) -> Result<Message> {
ensure!( ensure!(
!id.is_special(), !id.is_special(),
"Can not load special message ID {} from DB.", "Can not load special message ID {} from DB",
id id
); );
let msg = context let msg = context
@@ -340,8 +261,6 @@ impl Message {
" m.id AS id,", " m.id AS id,",
" rfc724_mid AS rfc724mid,", " rfc724_mid AS rfc724mid,",
" m.mime_in_reply_to AS mime_in_reply_to,", " m.mime_in_reply_to AS mime_in_reply_to,",
" m.server_folder AS server_folder,",
" m.server_uid AS server_uid,",
" m.chat_id AS chat_id,", " m.chat_id AS chat_id,",
" m.from_id AS from_id,", " m.from_id AS from_id,",
" m.to_id AS to_id,", " m.to_id AS to_id,",
@@ -392,8 +311,6 @@ impl Message {
in_reply_to: row in_reply_to: row
.get::<_, Option<String>>("mime_in_reply_to")? .get::<_, Option<String>>("mime_in_reply_to")?
.and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()), .and_then(|in_reply_to| parse_message_id(&in_reply_to).ok()),
server_folder: row.get::<_, Option<String>>("server_folder")?,
server_uid: row.get("server_uid")?,
chat_id: row.get("chat_id")?, chat_id: row.get("chat_id")?,
from_id: row.get("from_id")?, from_id: row.get("from_id")?,
to_id: row.get("to_id")?, to_id: row.get("to_id")?,
@@ -879,7 +796,7 @@ impl Message {
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> { pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>> {
if self.param.get(Param::Quote).is_some() && !self.is_forwarded() { if self.param.get(Param::Quote).is_some() && !self.is_forwarded() {
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)) = 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(context, msg_id).await?; let msg = Message::load_from_db(context, msg_id).await?;
return if msg.chat_id.is_trash() { return if msg.chat_id.is_trash() {
// If message is already moved to trash chat, pretend it does not exist. // If message is already moved to trash chat, pretend it does not exist.
@@ -1167,17 +1084,12 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
if !msg.rfc724_mid.is_empty() { if !msg.rfc724_mid.is_empty() {
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid); ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
} }
if let Some(ref server_folder) = msg.server_folder {
if !server_folder.is_empty() {
ret += &format!("\nLast seen as: {}/{}\n", server_folder, msg.server_uid);
}
}
let hop_info: Option<String> = context let hop_info: Option<String> = context
.sql .sql
.query_get_value("SELECT hop_info FROM msgs WHERE id=?;", paramsv![msg_id]) .query_get_value("SELECT hop_info FROM msgs WHERE id=?;", paramsv![msg_id])
.await?; .await?;
ret += "\n"; ret += "\n\n";
ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned()); ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned());
Ok(ret) Ok(ret)
@@ -1295,9 +1207,11 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
.trash(context) .trash(context)
.await .await
.with_context(|| format!("Unable to trash message {}", msg_id))?; .with_context(|| format!("Unable to trash message {}", msg_id))?;
job::add( context
context, .sql
job::Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0), .execute(
"UPDATE imap SET target='' WHERE rfc724_mid=?",
paramsv![msg.rfc724_mid],
) )
.await?; .await?;
} }
@@ -1314,6 +1228,11 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
) )
.await?; .await?;
} }
// Interrupt Inbox loop to start message deletion.
context
.interrupt_inbox(InterruptInfo::new(false, None))
.await;
Ok(()) Ok(())
} }
@@ -1694,7 +1613,7 @@ pub async fn estimate_deletion_cnt(
WHERE m.id > ? WHERE m.id > ?
AND timestamp < ? AND timestamp < ?
AND chat_id != ? AND chat_id != ?
AND server_uid != 0;", AND EXISTS (SELECT * FROM imap WHERE rfc724_mid=m.rfc724_mid);",
paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id], paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id],
) )
.await? .await?
@@ -1720,32 +1639,10 @@ pub async fn estimate_deletion_cnt(
Ok(cnt) Ok(cnt)
} }
/// Counts number of database records pointing to specified
/// Message-ID.
///
/// Unlinked messages are excluded.
pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> usize {
// check the number of messages with the same rfc724_mid
match context
.sql
.count(
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0",
paramsv![rfc724_mid],
)
.await
{
Ok(res) => res,
Err(err) => {
error!(context, "dc_get_rfc724_mid_cnt() failed. {}", err);
0
}
}
}
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<(String, u32, MsgId)>> { ) -> Result<Option<MsgId>> {
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");
@@ -1755,14 +1652,12 @@ pub(crate) async fn rfc724_mid_exists(
let res = context let res = context
.sql .sql
.query_row_optional( .query_row_optional(
"SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?", "SELECT id FROM msgs WHERE rfc724_mid=?",
paramsv![rfc724_mid], paramsv![rfc724_mid],
|row| { |row| {
let server_folder = row.get::<_, Option<String>>(0)?.unwrap_or_default(); let msg_id: MsgId = row.get(0)?;
let server_uid = row.get(1)?;
let msg_id: MsgId = row.get(2)?;
Ok((server_folder, server_uid, msg_id)) Ok(msg_id)
}, },
) )
.await?; .await?;
@@ -1770,28 +1665,6 @@ pub(crate) async fn rfc724_mid_exists(
Ok(res) Ok(res)
} }
pub async fn update_server_uid(
context: &Context,
rfc724_mid: &str,
server_folder: &str,
server_uid: u32,
) {
match context
.sql
.execute(
"UPDATE msgs SET server_folder=?, server_uid=? \
WHERE rfc724_mid=?",
paramsv![server_folder, server_uid, rfc724_mid],
)
.await
{
Ok(_) => {}
Err(err) => {
warn!(context, "msg: failed to update server_uid: {}", err);
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -1810,212 +1683,6 @@ mod tests {
); );
} }
// chat_msg means that the message was sent by Delta Chat
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
("INBOX", false, false, "INBOX"),
("INBOX", false, true, "INBOX"),
("INBOX", true, false, "INBOX"),
("INBOX", true, true, "DeltaChat"),
("Sent", false, false, "Sent"),
("Sent", false, true, "Sent"),
("Sent", true, false, "Sent"),
("Sent", true, true, "DeltaChat"),
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
("Spam", false, true, "INBOX"),
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
("Spam", true, true, "DeltaChat"),
];
// These are the same as above, but all messages in Spam stay in Spam
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
("INBOX", false, false, "INBOX"),
("INBOX", false, true, "INBOX"),
("INBOX", true, false, "INBOX"),
("INBOX", true, true, "DeltaChat"),
("Sent", false, false, "Sent"),
("Sent", false, true, "Sent"),
("Sent", true, false, "Sent"),
("Sent", true, true, "DeltaChat"),
("Spam", false, false, "Spam"),
("Spam", false, true, "Spam"),
("Spam", true, false, "Spam"),
("Spam", true, true, "Spam"),
];
#[async_std::test]
async fn test_needs_move_incoming_accepted() {
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
check_needs_move_combination(
folder,
*mvbox_move,
*chat_msg,
expected_destination,
true,
false,
false,
false,
)
.await;
}
}
#[async_std::test]
async fn test_needs_move_incoming_request() {
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
check_needs_move_combination(
folder,
*mvbox_move,
*chat_msg,
expected_destination,
false,
false,
false,
false,
)
.await;
}
}
#[async_std::test]
async fn test_needs_move_outgoing() {
for sentbox_move in &[true, false] {
// Test outgoing emails
for (folder, mvbox_move, chat_msg, mut expected_destination) in
COMBINATIONS_ACCEPTED_CHAT
{
if *folder == "INBOX" && !mvbox_move && *chat_msg && *sentbox_move {
expected_destination = "Sent"
}
check_needs_move_combination(
folder,
*mvbox_move,
*chat_msg,
expected_destination,
true,
true,
false,
*sentbox_move,
)
.await;
}
}
}
#[async_std::test]
async fn test_needs_move_setupmsg() {
// Test setupmessages
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
check_needs_move_combination(
folder,
*mvbox_move,
*chat_msg,
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
false,
true,
true,
false,
)
.await;
}
}
#[allow(clippy::too_many_arguments)]
async fn check_needs_move_combination(
folder: &str,
mvbox_move: bool,
chat_msg: bool,
expected_destination: &str,
accepted_chat: bool,
outgoing: bool,
setupmessage: bool,
sentbox_move: bool,
) {
println!("Testing: For folder {}, mvbox_move {}, chat_msg {}, accepted {}, outgoing {}, setupmessage {}",
folder, mvbox_move, chat_msg, accepted_chat, outgoing, setupmessage);
let t = TestContext::new_alice().await;
t.ctx
.set_config(Config::ConfiguredSpamFolder, Some("Spam"))
.await
.unwrap();
t.ctx
.set_config(Config::ConfiguredMvboxFolder, Some("DeltaChat"))
.await
.unwrap();
t.ctx
.set_config(Config::ConfiguredSentboxFolder, Some("Sent"))
.await
.unwrap();
t.ctx
.set_config(Config::MvboxMove, Some(if mvbox_move { "1" } else { "0" }))
.await
.unwrap();
t.ctx
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
t.ctx
.set_config_bool(Config::SentboxMove, sentbox_move)
.await
.unwrap();
if accepted_chat {
let contact_id = Contact::create(&t.ctx, "", "bob@example.net")
.await
.unwrap();
ChatId::create_for_contact(&t.ctx, contact_id)
.await
.unwrap();
}
let temp;
dc_receive_imf(
&t.ctx,
if setupmessage {
include_bytes!("../test-data/message/AutocryptSetupMessage.eml")
} else {
temp = format!(
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
{}\
Subject: foo\n\
Message-ID: <abc@example.com>\n\
{}\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
if outgoing {
"From: alice@example.org\nTo: bob@example.net\n"
} else {
"From: bob@example.net\nTo: alice@example.org\n"
},
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
);
temp.as_bytes()
},
folder,
1,
false,
)
.await
.unwrap();
let exists = rfc724_mid_exists(&t, "abc@example.com").await.unwrap();
let (folder_1, _, msg_id) = exists.unwrap();
assert_eq!(folder, folder_1);
let actual = if let Some(config) = msg_id.needs_move(&t.ctx, folder).await.unwrap() {
t.ctx.get_config(config).await.unwrap()
} else {
None
};
let expected = if expected_destination == folder {
None
} else {
Some(expected_destination)
};
assert_eq!(expected, actual.as_deref(), "For folder {}, mvbox_move {}, chat_msg {}, accepted {}, outgoing {}, setupmessage {}: expected {:?}, got {:?}",
folder, mvbox_move, chat_msg, accepted_chat, outgoing, setupmessage, expected, actual);
}
#[async_std::test] #[async_std::test]
async fn test_prepare_message_and_send() { async fn test_prepare_message_and_send() {
use crate::config::Config; use crate::config::Config;
@@ -2206,7 +1873,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
123,
false, false,
) )
.await .await
@@ -2416,7 +2082,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -2435,7 +2100,6 @@ mod tests {
\n\ \n\
hello again\n", hello again\n",
"INBOX", "INBOX",
2,
false, false,
) )
.await?; .await?;

View File

@@ -1637,7 +1637,6 @@ mod tests {
\n\ \n\
hello\n", hello\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -1731,7 +1730,6 @@ mod tests {
) )
.as_bytes(), .as_bytes(),
"INBOX", "INBOX",
5,
false, false,
) )
.await?; .await?;
@@ -1843,7 +1841,6 @@ mod tests {
\n\ \n\
Some other, completely unrelated content\n", Some other, completely unrelated content\n",
"INBOX", "INBOX",
2,
false, false,
) )
.await .await
@@ -1868,7 +1865,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
dc_receive_imf(context, imf_raw, "INBOX", 1, false) dc_receive_imf(context, imf_raw, "INBOX", false)
.await .await
.unwrap(); .unwrap();

View File

@@ -2855,7 +2855,6 @@ On 2020-10-25, Bob wrote:
&t.ctx, &t.ctx,
include_bytes!("../test-data/message/subj_with_multimedia_msg.eml"), include_bytes!("../test-data/message/subj_with_multimedia_msg.eml"),
"INBOX", "INBOX",
1,
false, false,
) )
.await .await
@@ -3004,7 +3003,7 @@ Subject: ...
Some quote. Some quote.
"###; "###;
dc_receive_imf(&t, raw, "INBOX", 1, false).await?; dc_receive_imf(&t, raw, "INBOX", false).await?;
// Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long. // Delta Chat generates In-Reply-To with a starting tab when Message-ID is too long.
let raw = br###"In-Reply-To: let raw = br###"In-Reply-To:
@@ -3021,7 +3020,7 @@ Subject: ...
Some reply Some reply
"###; "###;
dc_receive_imf(&t, raw, "INBOX", 2, false).await?; dc_receive_imf(&t, raw, "INBOX", false).await?;
let msg = t.get_last_msg().await; let msg = t.get_last_msg().await;
assert_eq!(msg.get_text().unwrap(), "Some reply"); assert_eq!(msg.get_text().unwrap(), "Some reply");
@@ -3049,13 +3048,13 @@ Message.
"###; "###;
// Bob receives message. // Bob receives message.
dc_receive_imf(&bob, raw, "INBOX", 1, false).await?; dc_receive_imf(&bob, raw, "INBOX", false).await?;
let msg = bob.get_last_msg().await; let msg = bob.get_last_msg().await;
// Message is incoming. // Message is incoming.
assert!(msg.param.get_bool(Param::WantsMdn).unwrap()); assert!(msg.param.get_bool(Param::WantsMdn).unwrap());
// Alice receives copy-to-self. // Alice receives copy-to-self.
dc_receive_imf(&alice, raw, "INBOX", 1, false).await?; dc_receive_imf(&alice, raw, "INBOX", false).await?;
let msg = alice.get_last_msg().await; let msg = alice.get_last_msg().await;
// Message is outgoing, don't send read receipt to self. // Message is outgoing, don't send read receipt to self.
assert!(msg.param.get_bool(Param::WantsMdn).is_none()); assert!(msg.param.get_bool(Param::WantsMdn).is_none());
@@ -3082,7 +3081,6 @@ Message.
hello\n" hello\n"
.as_bytes(), .as_bytes(),
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -3121,7 +3119,6 @@ Message.
--SNIPP--" --SNIPP--"
.as_bytes(), .as_bytes(),
"INBOX", "INBOX",
2,
false, false,
) )
.await?; .await?;

View File

@@ -82,11 +82,15 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
let mut jobs_loaded = 0; let mut jobs_loaded = 0;
let mut info = InterruptInfo::default(); let mut info = InterruptInfo::default();
loop { loop {
match job::load_next(&ctx, Thread::Imap, &info) let job = match job::load_next(&ctx, Thread::Imap, &info).await {
.await Err(err) => {
.ok() error!(ctx, "Failed loading job from the database: {:#}.", err);
.flatten() None
{ }
Ok(job) => job,
};
match job {
Some(job) if jobs_loaded <= 20 => { Some(job) if jobs_loaded <= 20 => {
jobs_loaded += 1; jobs_loaded += 1;
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await; job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
@@ -107,12 +111,6 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
None => { None => {
jobs_loaded = 0; jobs_loaded = 0;
// Expunge folder if needed, e.g. if some jobs have
// deleted messages on the server.
if let Err(err) = connection.maybe_close_folder(&ctx).await {
warn!(ctx, "failed to close folder: {:?}", err);
}
maybe_add_time_based_warnings(&ctx).await; maybe_add_time_based_warnings(&ctx).await;
info = if ctx info = if ctx
@@ -157,7 +155,7 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
} }
// fetch // fetch
if let Err(err) = connection.fetch(ctx, &watch_folder).await { if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await; connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err); warn!(ctx, "{:#}", err);
} }
@@ -183,13 +181,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
return connection.fake_idle(ctx, Some(watch_folder)).await; return connection.fake_idle(ctx, Some(watch_folder)).await;
} }
// fetch // Scan other folders before fetching from watched folder. This may result in the
if let Err(err) = connection.fetch(ctx, &watch_folder).await { // messages being moved into the watched folder, for example from the Spam folder to
connection.trigger_reconnect(ctx).await; // the Inbox folder.
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false, None);
}
if folder == Config::ConfiguredInboxFolder { if folder == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages // Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
if let Err(err) = connection.scan_folders(ctx).await { if let Err(err) = connection.scan_folders(ctx).await {
@@ -199,6 +193,13 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
} }
} }
// fetch
if let Err(err) = connection.fetch_move_delete(ctx, &watch_folder).await {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false, None);
}
connection.connectivity.set_connected(ctx).await; connection.connectivity.set_connected(ctx).await;
// idle // idle

View File

@@ -666,9 +666,11 @@ async fn maybe_add_from_param(
/// have a server UID. /// have a server UID.
async fn prune_tombstones(sql: &Sql) -> Result<()> { async fn prune_tombstones(sql: &Sql) -> Result<()> {
sql.execute( sql.execute(
"DELETE FROM msgs \ "DELETE FROM msgs
WHERE (chat_id = ? OR hidden) \ WHERE (chat_id=? OR hidden)
AND server_uid = 0", AND NOT EXISTS (
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
)",
paramsv![DC_CHAT_ID_TRASH], paramsv![DC_CHAT_ID_TRASH],
) )
.await?; .await?;

View File

@@ -502,6 +502,40 @@ item TEXT DEFAULT '');"#,
sql.execute_migration("ALTER TABLE msgs ADD COLUMN hop_info TEXT;", 81) sql.execute_migration("ALTER TABLE msgs ADD COLUMN hop_info TEXT;", 81)
.await?; .await?;
} }
if dbversion < 82 {
info!(context, "[migration] v82");
sql.execute_migration(
r#"CREATE TABLE imap (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rfc724_mid TEXT DEFAULT '', -- Message-ID header
folder TEXT DEFAULT '', -- IMAP folder
target TEXT DEFAULT '', -- Destination folder, empty to delete.
uid INTEGER DEFAULT 0, -- UID
uidvalidity INTEGER DEFAULT 0,
UNIQUE (folder, uid, uidvalidity)
);
CREATE INDEX imap_folder ON imap(folder);
CREATE INDEX imap_messageid ON imap(rfc724_mid);
INSERT INTO imap
(rfc724_mid, folder, target, uid, uidvalidity)
SELECT
rfc724_mid,
server_folder AS folder,
server_folder AS target,
server_uid AS uid,
(SELECT uidvalidity FROM imap_sync WHERE folder=server_folder) AS uidvalidity
FROM msgs
WHERE server_uid>0
ON CONFLICT (folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target;
"#,
82,
)
.await?;
}
Ok(( Ok((
recalc_fingerprints, recalc_fingerprints,
update_icons, update_icons,

View File

@@ -52,8 +52,6 @@ pub(crate) struct TestContext {
pub ctx: Context, pub ctx: Context,
pub dir: TempDir, pub dir: TempDir,
pub evtracker: EvTracker, pub evtracker: EvTracker,
/// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only.
recv_idx: RwLock<u32>,
/// Functions to call for events received. /// Functions to call for events received.
event_sinks: Arc<RwLock<Vec<Box<EventSink>>>>, event_sinks: Arc<RwLock<Vec<Box<EventSink>>>>,
/// Receives panics from sinks ("sink" means "event handler" here) /// Receives panics from sinks ("sink" means "event handler" here)
@@ -65,7 +63,6 @@ impl fmt::Debug for TestContext {
f.debug_struct("TestContext") f.debug_struct("TestContext")
.field("ctx", &self.ctx) .field("ctx", &self.ctx)
.field("dir", &self.dir) .field("dir", &self.dir)
.field("recv_idx", &self.recv_idx)
.field("event_sinks", &String::from("Vec<EventSink>")) .field("event_sinks", &String::from("Vec<EventSink>"))
.finish() .finish()
} }
@@ -141,7 +138,6 @@ impl TestContext {
ctx, ctx,
dir, dir,
evtracker: EvTracker(evtracker_receiver), evtracker: EvTracker(evtracker_receiver),
recv_idx: RwLock::new(0),
event_sinks, event_sinks,
poison_receiver, poison_receiver,
} }
@@ -302,13 +298,11 @@ impl TestContext {
/// ///
/// Receives a message using the `dc_receive_imf()` pipeline. /// Receives a message using the `dc_receive_imf()` pipeline.
pub async fn recv_msg(&self, msg: &SentMessage) { pub async fn recv_msg(&self, msg: &SentMessage) {
let mut idx = self.recv_idx.write().await;
*idx += 1;
let received_msg = let received_msg =
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n" "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n"
.to_owned() .to_owned()
+ &msg.payload(); + &msg.payload();
dc_receive_imf(&self.ctx, received_msg.as_bytes(), "INBOX", *idx, false) dc_receive_imf(&self.ctx, received_msg.as_bytes(), "INBOX", false)
.await .await
.unwrap(); .unwrap();
} }

View File

@@ -100,7 +100,6 @@ mod tests {
\n\ \n\
second message\n", second message\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -115,7 +114,6 @@ mod tests {
\n\ \n\
first message\n", first message\n",
"INBOX", "INBOX",
2,
false, false,
) )
.await?; .await?;
@@ -146,7 +144,6 @@ mod tests {
\n\ \n\
first message\n", first message\n",
"INBOX", "INBOX",
1,
false, false,
) )
.await?; .await?;
@@ -167,7 +164,6 @@ mod tests {
\n\ \n\
third message\n", third message\n",
"INBOX", "INBOX",
2,
false, false,
) )
.await?; .await?;
@@ -184,7 +180,6 @@ mod tests {
\n\ \n\
second message\n", second message\n",
"INBOX", "INBOX",
3,
false, false,
) )
.await?; .await?;