Skip to content

Commit ec46e6a

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. Part of chatmail/scalable-resilient-chat#13.
1 parent f04c881 commit ec46e6a

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
@@ -1964,21 +1964,23 @@ impl Session {
19641964
}
19651965
}
19661966

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

@@ -2037,7 +2039,8 @@ async fn spam_target_folder_cfg(
20372039
return Ok(None);
20382040
}
20392041

2040-
if needs_move_to_mvbox(context, headers).await?
2042+
if is_encrypted(headers) && context.get_config_bool(Config::MvboxMove).await?
2043+
|| needs_move_to_mvbox(context, headers).await?
20412044
// If OnlyFetchMvbox is set, we don't want to move the message to
20422045
// the inbox where we wouldn't fetch it again:
20432046
|| context.get_config_bool(Config::OnlyFetchMvbox).await?
@@ -2090,20 +2093,6 @@ async fn needs_move_to_mvbox(
20902093
context: &Context,
20912094
headers: &[mailparse::MailHeader<'_>],
20922095
) -> Result<bool> {
2093-
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2094-
if !context.get_config_bool(Config::IsChatmail).await?
2095-
&& has_chat_version
2096-
&& headers
2097-
.get_header_value(HeaderDef::AutoSubmitted)
2098-
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
2099-
.is_some()
2100-
{
2101-
if let Some(from) = mimeparser::get_from(headers) {
2102-
if context.is_self_addr(&from.addr).await? {
2103-
return Ok(true);
2104-
}
2105-
}
2106-
}
21072096
if !context.get_config_bool(Config::MvboxMove).await? {
21082097
return Ok(false);
21092098
}
@@ -2117,7 +2106,7 @@ async fn needs_move_to_mvbox(
21172106
return Ok(false);
21182107
}
21192108

2120-
if has_chat_version {
2109+
if headers.get_header_value(HeaderDef::SecureJoin).is_some() {
21212110
Ok(true)
21222111
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
21232112
match parent.is_dc_message {
@@ -2309,7 +2298,6 @@ pub(crate) async fn prefetch_should_download(
23092298
return Ok(false);
23102299
}
23112300

2312-
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
23132301
let accepted_contact = origin.is_known();
23142302
let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
23152303
.await?
@@ -2318,16 +2306,13 @@ pub(crate) async fn prefetch_should_download(
23182306
MessengerMessage::Yes | MessengerMessage::Reply => true,
23192307
})
23202308
.unwrap_or_default();
2321-
2322-
let show_emails =
2323-
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2324-
23252309
let show = is_autocrypt_setup_message
2326-
|| match show_emails {
2327-
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2328-
ShowEmails::AcceptedContacts => {
2329-
is_chat_message || is_reply_to_chat_message || accepted_contact
2330-
}
2310+
|| is_encrypted(headers)
2311+
|| match ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
2312+
.unwrap_or_default()
2313+
{
2314+
ShowEmails::Off => is_reply_to_chat_message,
2315+
ShowEmails::AcceptedContacts => is_reply_to_chat_message || accepted_contact,
23312316
ShowEmails::All => true,
23322317
};
23332318

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
@@ -1005,7 +1005,7 @@ pub(crate) async fn receive_imf_inner(
10051005
if let Some(is_bot) = mime_parser.is_bot {
10061006
// If the message is auto-generated and was generated by Delta Chat,
10071007
// mark the contact as a bot.
1008-
if mime_parser.get_header(HeaderDef::ChatVersion).is_some() {
1008+
if mime_parser.has_chat_version() {
10091009
from_id.mark_bot(context, is_bot).await?;
10101010
}
10111011
}
@@ -2947,7 +2947,7 @@ async fn apply_group_changes(
29472947
}
29482948

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

0 commit comments

Comments
 (0)