Skip to content

Commit 2519d18

Browse files
committed
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. 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`. We can't unconditionally move encrypted messages because the account may be shared with other software which doesn't and shouldn't look into DeltaChat.
1 parent ee75094 commit 2519d18

File tree

4 files changed

+54
-61
lines changed

4 files changed

+54
-61
lines changed

src/imap.rs

Lines changed: 22 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,21 +1954,23 @@ impl Session {
19541954
}
19551955
}
19561956

1957+
fn is_encrypted(headers: &[mailparse::MailHeader<'_>]) -> bool {
1958+
// Some MUAs use "multipart/mixed", so look also at Subject. We can't only look at Subject as
1959+
// it's not mandatory, see <https://datatracker.ietf.org/doc/html/rfc5322#section-3.6>.
1960+
let mut res = headers
1961+
.get_header_value(HeaderDef::ContentType)
1962+
.is_some_and(|v| v.contains("multipart/encrypted"));
1963+
res |= headers
1964+
.get_header_value(HeaderDef::Subject)
1965+
.is_some_and(|v| v == "..." || v == "[...]");
1966+
res
1967+
}
1968+
19571969
async fn should_move_out_of_spam(
19581970
context: &Context,
19591971
headers: &[mailparse::MailHeader<'_>],
19601972
) -> Result<bool> {
1961-
if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1962-
// If this is a chat message (i.e. has a ChatVersion header), then this might be
1963-
// a securejoin message. We can't find out at this point as we didn't prefetch
1964-
// the SecureJoin header. So, we always move chat messages out of Spam.
1965-
// Two possibilities to change this would be:
1966-
// 1. Remove the `&& !context.is_spam_folder(folder).await?` check from
1967-
// `fetch_new_messages()`, and then let `receive_imf()` check
1968-
// if it's a spam message and should be hidden.
1969-
// 2. Or add a flag to the ChatVersion header that this is a securejoin
1970-
// request, and return `true` here only if the message has this flag.
1971-
// `receive_imf()` can then check if the securejoin request is valid.
1973+
if headers.get_header_value(HeaderDef::SecureJoin).is_some() || is_encrypted(headers) {
19721974
return Ok(true);
19731975
}
19741976

@@ -2027,7 +2029,8 @@ async fn spam_target_folder_cfg(
20272029
return Ok(None);
20282030
}
20292031

2030-
if needs_move_to_mvbox(context, headers).await?
2032+
if is_encrypted(headers) && context.get_config_bool(Config::MvboxMove).await?
2033+
|| needs_move_to_mvbox(context, headers).await?
20312034
// If OnlyFetchMvbox is set, we don't want to move the message to
20322035
// the inbox where we wouldn't fetch it again:
20332036
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
@@ -2080,20 +2083,6 @@ async fn needs_move_to_mvbox(
20802083
context: &Context,
20812084
headers: &[mailparse::MailHeader<'_>],
20822085
) -> Result<bool> {
2083-
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2084-
if !context.get_config_bool(Config::IsChatmail).await?
2085-
&& has_chat_version
2086-
&& headers
2087-
.get_header_value(HeaderDef::AutoSubmitted)
2088-
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
2089-
.is_some()
2090-
{
2091-
if let Some(from) = mimeparser::get_from(headers) {
2092-
if context.is_self_addr(&from.addr).await? {
2093-
return Ok(true);
2094-
}
2095-
}
2096-
}
20972086
if !context.get_config_bool(Config::MvboxMove).await? {
20982087
return Ok(false);
20992088
}
@@ -2107,7 +2096,7 @@ async fn needs_move_to_mvbox(
21072096
return Ok(false);
21082097
}
21092098

2110-
if has_chat_version {
2099+
if headers.get_header_value(HeaderDef::SecureJoin).is_some() {
21112100
Ok(true)
21122101
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
21132102
match parent.is_dc_message {
@@ -2299,7 +2288,6 @@ pub(crate) async fn prefetch_should_download(
22992288
return Ok(false);
23002289
}
23012290

2302-
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
23032291
let accepted_contact = origin.is_known();
23042292
let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
23052293
.await?
@@ -2308,16 +2296,13 @@ pub(crate) async fn prefetch_should_download(
23082296
MessengerMessage::Yes | MessengerMessage::Reply => true,
23092297
})
23102298
.unwrap_or_default();
2311-
2312-
let show_emails =
2313-
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2314-
23152299
let show = is_autocrypt_setup_message
2316-
|| match show_emails {
2317-
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2318-
ShowEmails::AcceptedContacts => {
2319-
is_chat_message || is_reply_to_chat_message || accepted_contact
2320-
}
2300+
|| is_encrypted(headers)
2301+
|| match ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
2302+
.unwrap_or_default()
2303+
{
2304+
ShowEmails::Off => is_reply_to_chat_message,
2305+
ShowEmails::AcceptedContacts => is_reply_to_chat_message || accepted_contact,
23212306
ShowEmails::All => true,
23222307
};
23232308

src/imap/imap_tests.rs

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,14 @@ fn test_build_sequence_sets() {
9494
async fn check_target_folder_combination(
9595
folder: &str,
9696
mvbox_move: bool,
97-
chat_msg: bool,
97+
is_encrypted: bool,
9898
expected_destination: &str,
9999
accepted_chat: bool,
100100
outgoing: bool,
101101
setupmessage: bool,
102102
) -> Result<()> {
103103
println!(
104-
"Testing: For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"
104+
"Testing: For folder {folder}, mvbox_move {mvbox_move}, is_encrypted {is_encrypted}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}"
105105
);
106106

107107
let t = TestContext::new_alice().await;
@@ -124,7 +124,6 @@ async fn check_target_folder_combination(
124124
temp = format!(
125125
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
126126
{}\
127-
Subject: foo\n\
128127
Message-ID: <[email protected]>\n\
129128
{}\
130129
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
@@ -135,7 +134,11 @@ async fn check_target_folder_combination(
135134
} else {
136135
137136
},
138-
if chat_msg { "Chat-Version: 1.0\n" } else { "" },
137+
if is_encrypted {
138+
"Subject: [...]\n"
139+
} else {
140+
"Subject: foo\n"
141+
},
139142
);
140143
temp.as_bytes()
141144
};
@@ -157,30 +160,31 @@ async fn check_target_folder_combination(
157160
assert_eq!(
158161
expected,
159162
actual.as_deref(),
160-
"For folder {folder}, mvbox_move {mvbox_move}, chat_msg {chat_msg}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"
163+
"For folder {folder}, mvbox_move {mvbox_move}, is_encrypted {is_encrypted}, accepted {accepted_chat}, outgoing {outgoing}, setupmessage {setupmessage}: expected {expected:?}, got {actual:?}"
161164
);
162165
Ok(())
163166
}
164167

165-
// chat_msg means that the message was sent by Delta Chat
166-
// The tuples are (folder, mvbox_move, chat_msg, expected_destination)
168+
// The tuples are (folder, mvbox_move, is_encrypted, expected_destination)
167169
const COMBINATIONS_ACCEPTED_CHAT: &[(&str, bool, bool, &str)] = &[
168170
("INBOX", false, false, "INBOX"),
169171
("INBOX", false, true, "INBOX"),
170172
("INBOX", true, false, "INBOX"),
171-
("INBOX", true, true, "DeltaChat"),
172-
("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
173+
("INBOX", true, true, "INBOX"),
174+
("Spam", false, false, "INBOX"),
173175
("Spam", false, true, "INBOX"),
174-
("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
176+
// Move unencrypted emails in accepted chats from Spam to INBOX, not 100% sure on this, we could
177+
// also not move unencrypted emails or, if mvbox_move=1, move them to DeltaChat.
178+
("Spam", true, false, "INBOX"),
175179
("Spam", true, true, "DeltaChat"),
176180
];
177181

178-
// These are the same as above, but non-chat messages in Spam stay in Spam
182+
// These are the same as above, but unencrypted messages in Spam stay in Spam.
179183
const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
180184
("INBOX", false, false, "INBOX"),
181185
("INBOX", false, true, "INBOX"),
182186
("INBOX", true, false, "INBOX"),
183-
("INBOX", true, true, "DeltaChat"),
187+
("INBOX", true, true, "INBOX"),
184188
("Spam", false, false, "Spam"),
185189
("Spam", false, true, "INBOX"),
186190
("Spam", true, false, "Spam"),
@@ -189,11 +193,11 @@ const COMBINATIONS_REQUEST: &[(&str, bool, bool, &str)] = &[
189193

190194
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
191195
async fn test_target_folder_incoming_accepted() -> Result<()> {
192-
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
196+
for (folder, mvbox_move, is_encrypted, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
193197
check_target_folder_combination(
194198
folder,
195199
*mvbox_move,
196-
*chat_msg,
200+
*is_encrypted,
197201
expected_destination,
198202
true,
199203
false,
@@ -206,11 +210,11 @@ async fn test_target_folder_incoming_accepted() -> Result<()> {
206210

207211
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
208212
async fn test_target_folder_incoming_request() -> Result<()> {
209-
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
213+
for (folder, mvbox_move, is_encrypted, expected_destination) in COMBINATIONS_REQUEST {
210214
check_target_folder_combination(
211215
folder,
212216
*mvbox_move,
213-
*chat_msg,
217+
*is_encrypted,
214218
expected_destination,
215219
false,
216220
false,
@@ -224,11 +228,11 @@ async fn test_target_folder_incoming_request() -> Result<()> {
224228
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
225229
async fn test_target_folder_outgoing() -> Result<()> {
226230
// Test outgoing emails
227-
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
231+
for (folder, mvbox_move, is_encrypted, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
228232
check_target_folder_combination(
229233
folder,
230234
*mvbox_move,
231-
*chat_msg,
235+
*is_encrypted,
232236
expected_destination,
233237
true,
234238
true,
@@ -242,11 +246,11 @@ async fn test_target_folder_outgoing() -> Result<()> {
242246
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
243247
async fn test_target_folder_setupmsg() -> Result<()> {
244248
// Test setupmessages
245-
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
249+
for (folder, mvbox_move, is_encrypted, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
246250
check_target_folder_combination(
247251
folder,
248252
*mvbox_move,
249-
*chat_msg,
253+
*is_encrypted,
250254
if folder == &"Spam" { "INBOX" } else { folder }, // Never move setup messages, except if they are in "Spam"
251255
false,
252256
true,

src/imap/session.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@ use crate::tools;
1414
/// Prefetch:
1515
/// - Message-ID to check if we already have the message.
1616
/// - In-Reply-To and References to check if message is a reply to chat message.
17-
/// - Chat-Version to check if a message is a chat message
1817
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
1918
/// not necessarily sent by Delta Chat.
19+
///
20+
/// NB: We don't look at Chat-Version as we don't want any "better" handling for unencrypted
21+
/// messages.
2022
const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
2123
MESSAGE-ID \
2224
DATE \
2325
X-MICROSOFT-ORIGINAL-MESSAGE-ID \
2426
FROM \
2527
IN-REPLY-TO REFERENCES \
26-
CHAT-VERSION \
28+
CONTENT-TYPE \
29+
SECURE-JOIN \
30+
SUBJECT \
2731
AUTO-SUBMITTED \
2832
AUTOCRYPT-SETUP-MESSAGE\
2933
)])";

src/receive_imf.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,7 @@ pub(crate) async fn receive_imf_inner(
997997
if let Some(is_bot) = mime_parser.is_bot {
998998
// If the message is auto-generated and was generated by Delta Chat,
999999
// mark the contact as a bot.
1000-
if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
1000+
if mime_parser.has_chat_version() {
10011001
from_id.mark_bot(context, is_bot).await?;
10021002
}
10031003
}
@@ -2939,7 +2939,7 @@ async fn apply_group_changes(
29392939
}
29402940

29412941
// Allow non-Delta Chat MUAs to add members.
2942-
if mime_parser.get_header(HeaderDef::ChatVersion).is_none() {
2942+
if !mime_parser.has_chat_version() {
29432943
// Don't delete any members locally, but instead add absent ones to provide group
29442944
// membership consistency for all members:
29452945
new_members.extend(to_ids_flat.iter());

0 commit comments

Comments
 (0)