mirror of
https://github.com/chatmail/core.git
synced 2026-04-26 18:06:35 +03:00
feat: imap: Don't prefetch Chat-Version; try to find out message encryption state instead
Instead, prefetch Secure-Join, Content-Type and Subject headers, try to find out if the message is encrypted, i.e.: - if its Content-Type is "multipart/encrypted" - or Subject is "..." or "[...]" as some MUAs use "multipart/mixed"; we can't only look at Subject as it's not mandatory; and depending on this decide on the target folder and whether the message should be downloaded. There's no much sense in downloading unencrypted "Chat-Version"-containing messages if `ShowEmails` is `Off` or `AcceptedContacts`, unencrypted Delta Chat messages should be considered as usual emails, there's even the "New E-Mail" feature in UIs nowadays which sends such messages. Don't prefetch Auto-Submitted as well, this becomes unnecessary. Changed behavior: before, "Chat-Version"-containing messages were moved from INBOX to DeltaChat, now such encrypted messages may remain in INBOX -- if there's no parent message or it's not `MessengerMessage`. Don't unconditionally move encrypted messages yet because the account may be shared with other software which doesn't and shouldn't look into the DeltaChat folder.
This commit is contained in:
75
src/imap.rs
75
src/imap.rs
@@ -1965,21 +1965,24 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_encrypted(headers: &[mailparse::MailHeader<'_>]) -> bool {
|
||||||
|
let content_type = headers.get_header_value(HeaderDef::ContentType);
|
||||||
|
let content_type = content_type.as_ref();
|
||||||
|
let res = content_type.is_some_and(|v| v.contains("multipart/encrypted"));
|
||||||
|
// Some MUAs use "multipart/mixed", look also at Subject in this case. We can't only look at
|
||||||
|
// Subject as it's not mandatory (<https://datatracker.ietf.org/doc/html/rfc5322#section-3.6>)
|
||||||
|
// and may be user-formed.
|
||||||
|
res || content_type.is_some_and(|v| v.contains("multipart/mixed"))
|
||||||
|
&& headers
|
||||||
|
.get_header_value(HeaderDef::Subject)
|
||||||
|
.is_some_and(|v| v == "..." || v == "[...]")
|
||||||
|
}
|
||||||
|
|
||||||
async fn should_move_out_of_spam(
|
async fn should_move_out_of_spam(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
headers: &[mailparse::MailHeader<'_>],
|
headers: &[mailparse::MailHeader<'_>],
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
|
if headers.get_header_value(HeaderDef::SecureJoin).is_some() || is_encrypted(headers) {
|
||||||
// If this is a chat message (i.e. has a ChatVersion header), then this might be
|
|
||||||
// a securejoin message. We can't find out at this point as we didn't prefetch
|
|
||||||
// the SecureJoin header. So, we always move chat messages out of Spam.
|
|
||||||
// Two possibilities to change this would be:
|
|
||||||
// 1. Remove the `&& !context.is_spam_folder(folder).await?` check from
|
|
||||||
// `fetch_new_messages()`, and then let `receive_imf()` check
|
|
||||||
// if it's a spam message and should be hidden.
|
|
||||||
// 2. Or add a flag to the ChatVersion header that this is a securejoin
|
|
||||||
// request, and return `true` here only if the message has this flag.
|
|
||||||
// `receive_imf()` can then check if the securejoin request is valid.
|
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2038,7 +2041,8 @@ async fn spam_target_folder_cfg(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
if needs_move_to_mvbox(context, headers).await?
|
if is_encrypted(headers) && context.get_config_bool(Config::MvboxMove).await?
|
||||||
|
|| needs_move_to_mvbox(context, headers).await?
|
||||||
// If OnlyFetchMvbox is set, we don't want to move the message to
|
// If OnlyFetchMvbox is set, we don't want to move the message to
|
||||||
// the inbox where we wouldn't fetch it again:
|
// the inbox where we wouldn't fetch it again:
|
||||||
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
|
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
|
||||||
@@ -2091,18 +2095,6 @@ async fn needs_move_to_mvbox(
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
headers: &[mailparse::MailHeader<'_>],
|
headers: &[mailparse::MailHeader<'_>],
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
|
||||||
if !context.get_config_bool(Config::IsChatmail).await?
|
|
||||||
&& has_chat_version
|
|
||||||
&& headers
|
|
||||||
.get_header_value(HeaderDef::AutoSubmitted)
|
|
||||||
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
|
|
||||||
.is_some()
|
|
||||||
&& let Some(from) = mimeparser::get_from(headers)
|
|
||||||
&& context.is_self_addr(&from.addr).await?
|
|
||||||
{
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
if !context.get_config_bool(Config::MvboxMove).await? {
|
if !context.get_config_bool(Config::MvboxMove).await? {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
@@ -2116,7 +2108,7 @@ async fn needs_move_to_mvbox(
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_chat_version {
|
if headers.get_header_value(HeaderDef::SecureJoin).is_some() {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
|
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
|
||||||
match parent.is_dc_message {
|
match parent.is_dc_message {
|
||||||
@@ -2309,27 +2301,24 @@ pub(crate) async fn prefetch_should_download(
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
|
||||||
let accepted_contact = origin.is_known();
|
let accepted_contact = origin.is_known();
|
||||||
let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
|
|
||||||
.await?
|
|
||||||
.map(|parent| match parent.is_dc_message {
|
|
||||||
MessengerMessage::No => false,
|
|
||||||
MessengerMessage::Yes | MessengerMessage::Reply => true,
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let show_emails =
|
|
||||||
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
|
|
||||||
|
|
||||||
let show = is_autocrypt_setup_message
|
let show = is_autocrypt_setup_message
|
||||||
|| match show_emails {
|
|| headers.get_header_value(HeaderDef::SecureJoin).is_some()
|
||||||
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
|
|| is_encrypted(headers)
|
||||||
ShowEmails::AcceptedContacts => {
|
|| match ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
|
||||||
is_chat_message || is_reply_to_chat_message || accepted_contact
|
.unwrap_or_default()
|
||||||
}
|
{
|
||||||
|
ShowEmails::Off => false,
|
||||||
|
ShowEmails::AcceptedContacts => accepted_contact,
|
||||||
ShowEmails::All => true,
|
ShowEmails::All => true,
|
||||||
};
|
}
|
||||||
|
|| get_prefetch_parent_message(context, headers)
|
||||||
|
.await?
|
||||||
|
.map(|parent| match parent.is_dc_message {
|
||||||
|
MessengerMessage::No => false,
|
||||||
|
MessengerMessage::Yes | MessengerMessage::Reply => true,
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let should_download = (show && !blocked_contact) || maybe_ndn;
|
let should_download = (show && !blocked_contact) || maybe_ndn;
|
||||||
Ok(should_download)
|
Ok(should_download)
|
||||||
|
|||||||
@@ -94,14 +94,14 @@ fn test_build_sequence_sets() {
|
|||||||
async fn check_target_folder_combination(
|
async fn check_target_folder_combination(
|
||||||
folder: &str,
|
folder: &str,
|
||||||
mvbox_move: bool,
|
mvbox_move: bool,
|
||||||
chat_msg: bool,
|
is_encrypted: bool,
|
||||||
expected_destination: &str,
|
expected_destination: &str,
|
||||||
accepted_chat: bool,
|
accepted_chat: bool,
|
||||||
outgoing: bool,
|
outgoing: bool,
|
||||||
setupmessage: bool,
|
setupmessage: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"
|
"Testing: For folder {folder}, mvbox_move {mvbox_move}, is_encrypted {is_encrypted}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"
|
||||||
);
|
);
|
||||||
|
|
||||||
let t = TestContext::new_alice().await;
|
let t = TestContext::new_alice().await;
|
||||||
@@ -124,7 +124,6 @@ async fn check_target_folder_combination(
|
|||||||
temp = format!(
|
temp = format!(
|
||||||
"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\
|
||||||
{}\
|
{}\
|
||||||
Subject: foo\n\
|
|
||||||
Message-ID: <abc@example.com>\n\
|
Message-ID: <abc@example.com>\n\
|
||||||
{}\
|
{}\
|
||||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||||
@@ -135,7 +134,12 @@ async fn check_target_folder_combination(
|
|||||||
} else {
|
} else {
|
||||||
"From: bob@example.net\nTo: alice@example.org\n"
|
"From: bob@example.net\nTo: alice@example.org\n"
|
||||||
},
|
},
|
||||||
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
|
if is_encrypted {
|
||||||
|
"Subject: [...]\n\
|
||||||
|
Content-Type: multipart/mixed; boundary=\"someboundary\"\n"
|
||||||
|
} else {
|
||||||
|
"Subject: foo\n"
|
||||||
|
},
|
||||||
);
|
);
|
||||||
temp.as_bytes()
|
temp.as_bytes()
|
||||||
};
|
};
|
||||||
@@ -157,30 +161,31 @@ async fn check_target_folder_combination(
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
expected,
|
expected,
|
||||||
actual.as_deref(),
|
actual.as_deref(),
|
||||||
"For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"
|
"For folder {folder}, mvbox_move {mvbox_move}, is_encrypted {is_encrypted}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// chat_msg means that the message was sent by Delta Chat
|
// The tuples are (folder, mvbox_move, is_encrypted, expected_destination)
|
||||||
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
|
|
||||||
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
|
||||||
("INBOX", false, false, "INBOX"),
|
("INBOX", false, false, "INBOX"),
|
||||||
("INBOX", false, true, "INBOX"),
|
("INBOX", false, true, "INBOX"),
|
||||||
("INBOX", true, false, "INBOX"),
|
("INBOX", true, false, "INBOX"),
|
||||||
("INBOX", true, true, "DeltaChat"),
|
("INBOX", true, true, "INBOX"),
|
||||||
("Spam", false, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
("Spam", false, false, "INBOX"),
|
||||||
("Spam", false, true, "INBOX"),
|
("Spam", false, true, "INBOX"),
|
||||||
("Spam", true, false, "INBOX"), // Move classical emails in accepted chats from Spam to Inbox, not 100% sure on this, we could also just never move non-chat-msgs
|
// Move unencrypted emails in accepted chats from Spam to INBOX, not 100% sure on this, we could
|
||||||
|
// also not move unencrypted emails or, if mvbox_move=1, move them to DeltaChat.
|
||||||
|
("Spam", true, false, "INBOX"),
|
||||||
("Spam", true, true, "DeltaChat"),
|
("Spam", true, true, "DeltaChat"),
|
||||||
];
|
];
|
||||||
|
|
||||||
// These are the same as above, but non-chat messages in Spam stay in Spam
|
// These are the same as above, but unencrypted messages in Spam stay in Spam.
|
||||||
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
||||||
("INBOX", false, false, "INBOX"),
|
("INBOX", false, false, "INBOX"),
|
||||||
("INBOX", false, true, "INBOX"),
|
("INBOX", false, true, "INBOX"),
|
||||||
("INBOX", true, false, "INBOX"),
|
("INBOX", true, false, "INBOX"),
|
||||||
("INBOX", true, true, "DeltaChat"),
|
("INBOX", true, true, "INBOX"),
|
||||||
("Spam", false, false, "Spam"),
|
("Spam", false, false, "Spam"),
|
||||||
("Spam", false, true, "INBOX"),
|
("Spam", false, true, "INBOX"),
|
||||||
("Spam", true, false, "Spam"),
|
("Spam", true, false, "Spam"),
|
||||||
@@ -189,11 +194,11 @@ const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_target_folder_incoming_accepted() -> Result<()> {
|
async fn test_target_folder_incoming_accepted() -> Result<()> {
|
||||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
for (folder, mvbox_move, is_encrypted, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||||
check_target_folder_combination(
|
check_target_folder_combination(
|
||||||
folder,
|
folder,
|
||||||
*mvbox_move,
|
*mvbox_move,
|
||||||
*chat_msg,
|
*is_encrypted,
|
||||||
expected_destination,
|
expected_destination,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
@@ -206,11 +211,11 @@ async fn test_target_folder_incoming_accepted() -> Result<()> {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_target_folder_incoming_request() -> Result<()> {
|
async fn test_target_folder_incoming_request() -> Result<()> {
|
||||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
|
for (folder, mvbox_move, is_encrypted, expected_destination) in COMBINATIONS_REQUEST {
|
||||||
check_target_folder_combination(
|
check_target_folder_combination(
|
||||||
folder,
|
folder,
|
||||||
*mvbox_move,
|
*mvbox_move,
|
||||||
*chat_msg,
|
*is_encrypted,
|
||||||
expected_destination,
|
expected_destination,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -224,11 +229,11 @@ async fn test_target_folder_incoming_request() -> Result<()> {
|
|||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_target_folder_outgoing() -> Result<()> {
|
async fn test_target_folder_outgoing() -> Result<()> {
|
||||||
// Test outgoing emails
|
// Test outgoing emails
|
||||||
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
for (folder, mvbox_move, is_encrypted, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||||
check_target_folder_combination(
|
check_target_folder_combination(
|
||||||
folder,
|
folder,
|
||||||
*mvbox_move,
|
*mvbox_move,
|
||||||
*chat_msg,
|
*is_encrypted,
|
||||||
expected_destination,
|
expected_destination,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
@@ -242,11 +247,11 @@ async fn test_target_folder_outgoing() -> Result<()> {
|
|||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_target_folder_setupmsg() -> Result<()> {
|
async fn test_target_folder_setupmsg() -> Result<()> {
|
||||||
// Test setupmessages
|
// Test setupmessages
|
||||||
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
for (folder, mvbox_move, is_encrypted, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
|
||||||
check_target_folder_combination(
|
check_target_folder_combination(
|
||||||
folder,
|
folder,
|
||||||
*mvbox_move,
|
*mvbox_move,
|
||||||
*chat_msg,
|
*is_encrypted,
|
||||||
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -14,17 +14,20 @@ use crate::tools;
|
|||||||
/// Prefetch:
|
/// Prefetch:
|
||||||
/// - Message-ID to check if we already have the message.
|
/// - Message-ID to check if we already have the message.
|
||||||
/// - In-Reply-To and References to check if message is a reply to chat message.
|
/// - In-Reply-To and References to check if message is a reply to chat message.
|
||||||
/// - Chat-Version to check if a message is a chat message
|
|
||||||
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
|
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
|
||||||
/// not necessarily sent by Delta Chat.
|
/// not necessarily sent by Delta Chat.
|
||||||
|
///
|
||||||
|
/// NB: We don't look at Chat-Version as we don't want any "better" handling for unencrypted
|
||||||
|
/// messages.
|
||||||
const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
|
const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
|
||||||
MESSAGE-ID \
|
MESSAGE-ID \
|
||||||
DATE \
|
DATE \
|
||||||
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
|
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
|
||||||
FROM \
|
FROM \
|
||||||
IN-REPLY-TO REFERENCES \
|
IN-REPLY-TO REFERENCES \
|
||||||
CHAT-VERSION \
|
CONTENT-TYPE \
|
||||||
AUTO-SUBMITTED \
|
SECURE-JOIN \
|
||||||
|
SUBJECT \
|
||||||
AUTOCRYPT-SETUP-MESSAGE\
|
AUTOCRYPT-SETUP-MESSAGE\
|
||||||
)])";
|
)])";
|
||||||
|
|
||||||
|
|||||||
@@ -979,7 +979,7 @@ pub(crate) async fn receive_imf_inner(
|
|||||||
if let Some(is_bot) = mime_parser.is_bot {
|
if let Some(is_bot) = mime_parser.is_bot {
|
||||||
// If the message is auto-generated and was generated by Delta Chat,
|
// If the message is auto-generated and was generated by Delta Chat,
|
||||||
// mark the contact as a bot.
|
// mark the contact as a bot.
|
||||||
if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
|
if mime_parser.has_chat_version() {
|
||||||
from_id.mark_bot(context, is_bot).await?;
|
from_id.mark_bot(context, is_bot).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2924,7 +2924,7 @@ async fn apply_group_changes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allow non-Delta Chat MUAs to add members.
|
// Allow non-Delta Chat MUAs to add members.
|
||||||
if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
|
if !mime_parser.has_chat_version() {
|
||||||
// Don't delete any members locally, but instead add absent ones to provide group
|
// Don't delete any members locally, but instead add absent ones to provide group
|
||||||
// membership consistency for all members:
|
// membership consistency for all members:
|
||||||
new_members.extend(to_ids_flat.iter());
|
new_members.extend(to_ids_flat.iter());
|
||||||
|
|||||||
Reference in New Issue
Block a user