Skip to content

Commit 44227d7

Browse files
committed
fix: Put Message-ID into hidden headers and take it from there on receiver (#4798)
Put a copy of Message-ID into hidden headers and prefer it over the one in the IMF header section that servers mess up with. This also reverts "Set X-Microsoft-Original-Message-ID on outgoing emails for amazonaws (#3077)".
1 parent 6bcf022 commit 44227d7

File tree

5 files changed

+213
-76
lines changed

5 files changed

+213
-76
lines changed

src/chat.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5904,7 +5904,7 @@ mod tests {
59045904
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
59055905
let sent_msg = alice.pop_sent_msg().await;
59065906
let msg = sent_msg.payload();
5907-
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 1);
5907+
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 2);
59085908
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
59095909
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
59105910
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 0);

src/mimefactory.rs

Lines changed: 43 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ struct MessageHeaders {
124124
/// Headers that MUST NOT go into IMF header section.
125125
///
126126
/// These are large headers which may hit the header section size limit on the server, such as
127-
/// Chat-User-Avatar with a base64-encoded image inside.
127+
/// Chat-User-Avatar with a base64-encoded image inside. Also there are headers duplicated here
128+
/// that servers mess up with in the IMF header section, like Message-ID.
128129
pub hidden: Vec<Header>,
129130
}
130131

@@ -560,24 +561,9 @@ impl<'a> MimeFactory<'a> {
560561
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(None, &self.from_addr),
561562
};
562563
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
563-
564-
// Amazon's SMTP servers change the `Message-ID`, just as Outlook's SMTP servers do.
565-
// Outlook's servers add an `X-Microsoft-Original-Message-ID` header with the original `Message-ID`,
566-
// and when downloading messages we look for this header in order to correctly identify
567-
// messages.
568-
// Amazon's servers do not add such a header, so we just add it ourselves.
569-
if let Some(server) = context.get_config(Config::ConfiguredSendServer).await? {
570-
if server.ends_with(".amazonaws.com") {
571-
headers.unprotected.push(Header::new(
572-
"X-Microsoft-Original-Message-ID".into(),
573-
rfc724_mid_headervalue.clone(),
574-
))
575-
}
576-
}
577-
578-
headers
579-
.unprotected
580-
.push(Header::new("Message-ID".into(), rfc724_mid_headervalue));
564+
let rfc724_mid_header = Header::new("Message-ID".into(), rfc724_mid_headervalue);
565+
headers.unprotected.push(rfc724_mid_header.clone());
566+
headers.hidden.push(rfc724_mid_header);
581567

582568
// Reply headers as in <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2>.
583569
if !self.in_reply_to.is_empty() {
@@ -783,29 +769,22 @@ impl<'a> MimeFactory<'a> {
783769
)
784770
.header(("Subject".to_string(), "...".to_string()))
785771
} else {
786-
let message = if headers.hidden.is_empty() {
787-
message
788-
} else {
789-
// Store hidden headers in the inner unencrypted message.
790-
let message = headers
791-
.hidden
792-
.into_iter()
793-
.fold(message, |message, header| message.header(header));
794-
795-
PartBuilder::new()
796-
.message_type(MimeMultipartType::Mixed)
797-
.child(message.build())
798-
};
772+
// Store hidden headers in the inner unencrypted message.
773+
let message = headers
774+
.hidden
775+
.into_iter()
776+
.fold(message, |message, header| message.header(header));
777+
let message = PartBuilder::new()
778+
.message_type(MimeMultipartType::Mixed)
779+
.child(message.build());
799780

800781
// Store protected headers in the outer message.
801782
let message = headers
802783
.protected
803784
.iter()
804785
.fold(message, |message, header| message.header(header.clone()));
805786

806-
if self.should_skip_autocrypt()
807-
|| !context.get_config_bool(Config::SignUnencrypted).await?
808-
{
787+
if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? {
809788
let protected: HashSet<Header> = HashSet::from_iter(headers.protected.into_iter());
810789
for h in headers.unprotected.split_off(0) {
811790
if !protected.contains(&h) {
@@ -2165,33 +2144,37 @@ mod tests {
21652144
let body = payload.next().unwrap();
21662145

21672146
assert_eq!(outer.match_indices("multipart/mixed").count(), 1);
2147+
assert_eq!(outer.match_indices("Message-ID:").count(), 1);
21682148
assert_eq!(outer.match_indices("Subject:").count(), 1);
21692149
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
21702150
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
21712151

21722152
assert_eq!(inner.match_indices("text/plain").count(), 1);
2153+
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
21732154
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1);
21742155
assert_eq!(inner.match_indices("Subject:").count(), 0);
21752156

21762157
assert_eq!(body.match_indices("this is the text!").count(), 1);
21772158

21782159
// if another message is sent, that one must not contain the avatar
2179-
// and no artificial multipart/mixed nesting
21802160
let sent_msg = t.send_msg(chat.id, &mut msg).await;
2181-
let mut payload = sent_msg.payload().splitn(2, "\r\n\r\n");
2161+
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
21822162
let outer = payload.next().unwrap();
2163+
let inner = payload.next().unwrap();
21832164
let body = payload.next().unwrap();
21842165

2185-
assert_eq!(outer.match_indices("text/plain").count(), 1);
2166+
assert_eq!(outer.match_indices("multipart/mixed").count(), 1);
2167+
assert_eq!(outer.match_indices("Message-ID:").count(), 1);
21862168
assert_eq!(outer.match_indices("Subject:").count(), 1);
21872169
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
2188-
assert_eq!(outer.match_indices("multipart/mixed").count(), 0);
21892170
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
21902171

2172+
assert_eq!(inner.match_indices("text/plain").count(), 1);
2173+
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
2174+
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0);
2175+
assert_eq!(inner.match_indices("Subject:").count(), 0);
2176+
21912177
assert_eq!(body.match_indices("this is the text!").count(), 1);
2192-
assert_eq!(body.match_indices("text/plain").count(), 0);
2193-
assert_eq!(body.match_indices("Chat-User-Avatar:").count(), 0);
2194-
assert_eq!(body.match_indices("Subject:").count(), 0);
21952178

21962179
Ok(())
21972180
}
@@ -2223,6 +2206,7 @@ mod tests {
22232206
let part = payload.next().unwrap();
22242207
assert_eq!(part.match_indices("multipart/signed").count(), 1);
22252208
assert_eq!(part.match_indices("From:").count(), 1);
2209+
assert_eq!(part.match_indices("Message-ID:").count(), 1);
22262210
assert_eq!(part.match_indices("Subject:").count(), 0);
22272211
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
22282212
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
@@ -2234,13 +2218,15 @@ mod tests {
22342218
1
22352219
);
22362220
assert_eq!(part.match_indices("From:").count(), 1);
2221+
assert_eq!(part.match_indices("Message-ID:").count(), 0);
22372222
assert_eq!(part.match_indices("Subject:").count(), 1);
22382223
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
22392224
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
22402225

22412226
let part = payload.next().unwrap();
22422227
assert_eq!(part.match_indices("text/plain").count(), 1);
22432228
assert_eq!(part.match_indices("From:").count(), 0);
2229+
assert_eq!(part.match_indices("Message-ID:").count(), 1);
22442230
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1);
22452231
assert_eq!(part.match_indices("Subject:").count(), 0);
22462232

@@ -2261,31 +2247,38 @@ mod tests {
22612247
.is_some());
22622248

22632249
// if another message is sent, that one must not contain the avatar
2264-
// and no artificial multipart/mixed nesting
22652250
let sent_msg = t.send_msg(chat.id, &mut msg).await;
2266-
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
2251+
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
22672252

22682253
let part = payload.next().unwrap();
22692254
assert_eq!(part.match_indices("multipart/signed").count(), 1);
22702255
assert_eq!(part.match_indices("From:").count(), 1);
2256+
assert_eq!(part.match_indices("Message-ID:").count(), 1);
22712257
assert_eq!(part.match_indices("Subject:").count(), 0);
22722258
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
22732259
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
22742260

22752261
let part = payload.next().unwrap();
2276-
assert_eq!(part.match_indices("text/plain").count(), 1);
2262+
assert_eq!(
2263+
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
2264+
.count(),
2265+
1
2266+
);
22772267
assert_eq!(part.match_indices("From:").count(), 1);
2268+
assert_eq!(part.match_indices("Message-ID:").count(), 0);
22782269
assert_eq!(part.match_indices("Subject:").count(), 1);
22792270
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
2280-
assert_eq!(part.match_indices("multipart/mixed").count(), 0);
22812271
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
22822272

2273+
let part = payload.next().unwrap();
2274+
assert_eq!(part.match_indices("text/plain").count(), 1);
2275+
assert_eq!(body.match_indices("From:").count(), 0);
2276+
assert_eq!(part.match_indices("Message-ID:").count(), 1);
2277+
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
2278+
assert_eq!(part.match_indices("Subject:").count(), 0);
2279+
22832280
let body = payload.next().unwrap();
22842281
assert_eq!(body.match_indices("this is the text!").count(), 1);
2285-
assert_eq!(body.match_indices("text/plain").count(), 0);
2286-
assert_eq!(body.match_indices("From:").count(), 0);
2287-
assert_eq!(body.match_indices("Chat-User-Avatar:").count(), 0);
2288-
assert_eq!(body.match_indices("Subject:").count(), 0);
22892282

22902283
bob.recv_msg(&sent_msg).await;
22912284
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();

src/receive_imf.rs

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -184,28 +184,49 @@ pub(crate) async fn receive_imf_inner(
184184
}
185185
}
186186

187-
info!(context, "Receiving message {rfc724_mid:?}, seen={seen}...");
187+
let rfc724_mid_orig = &mime_parser
188+
.get_rfc724_mid()
189+
.unwrap_or(rfc724_mid.to_string());
190+
info!(
191+
context,
192+
"Receiving message {rfc724_mid_orig:?}, seen={seen}...",
193+
);
188194

189195
// check, if the mail is already in our database.
190196
// make sure, this check is done eg. before securejoin-processing.
191-
let (replace_msg_id, replace_chat_id) =
192-
if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
193-
let msg = Message::load_from_db(context, old_msg_id).await?;
194-
if msg.download_state() != DownloadState::Done && is_partial_download.is_none() {
195-
// the message was partially downloaded before and is fully downloaded now.
196-
info!(
197-
context,
198-
"Message already partly in DB, replacing by full message."
199-
);
200-
(Some(old_msg_id), Some(msg.chat_id))
201-
} else {
202-
// the message was probably moved around.
203-
info!(context, "Message already in DB, doing nothing.");
204-
return Ok(None);
205-
}
197+
let (replace_msg_id, replace_chat_id);
198+
if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
199+
if is_partial_download.is_some() {
200+
info!(
201+
context,
202+
"Got a partial download and message is already in DB."
203+
);
204+
return Ok(None);
205+
}
206+
let msg = Message::load_from_db(context, old_msg_id).await?;
207+
replace_msg_id = Some(old_msg_id);
208+
replace_chat_id = if msg.download_state() != DownloadState::Done {
209+
// the message was partially downloaded before and is fully downloaded now.
210+
info!(
211+
context,
212+
"Message already partly in DB, replacing by full message."
213+
);
214+
Some(msg.chat_id)
215+
} else {
216+
None
217+
};
218+
} else {
219+
replace_msg_id = if rfc724_mid_orig != rfc724_mid {
220+
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
206221
} else {
207-
(None, None)
222+
None
208223
};
224+
replace_chat_id = None;
225+
}
226+
if replace_msg_id.is_some() && replace_chat_id.is_none() {
227+
info!(context, "Message is already downloaded.");
228+
return Ok(None);
229+
};
209230

210231
let prevent_rename =
211232
mime_parser.is_mailinglist_message() || mime_parser.get_header(HeaderDef::Sender).is_some();
@@ -301,7 +322,7 @@ pub(crate) async fn receive_imf_inner(
301322
imf_raw,
302323
incoming,
303324
&to_ids,
304-
rfc724_mid,
325+
rfc724_mid_orig,
305326
from_id,
306327
seen || replace_msg_id.is_some(),
307328
is_partial_download,
@@ -421,20 +442,30 @@ pub(crate) async fn receive_imf_inner(
421442
let delete_server_after = context.get_config_delete_server_after().await?;
422443

423444
if !received_msg.msg_ids.is_empty() {
424-
if received_msg.needs_delete_job
445+
let target = if received_msg.needs_delete_job
425446
|| (delete_server_after == Some(0) && is_partial_download.is_none())
426447
{
427-
let target = context.get_delete_msgs_target().await?;
448+
Some(context.get_delete_msgs_target().await?)
449+
} else {
450+
None
451+
};
452+
if target.is_some() || rfc724_mid_orig != rfc724_mid {
453+
let target_subst = match &target {
454+
Some(target) => format!("target='{target}',"),
455+
None => "".to_string(),
456+
};
428457
context
429458
.sql
430459
.execute(
431-
"UPDATE imap SET target=? WHERE rfc724_mid=?",
432-
(target, rfc724_mid),
460+
&format!("UPDATE imap SET {target_subst} rfc724_mid=?1 WHERE rfc724_mid=?2"),
461+
(rfc724_mid_orig, rfc724_mid),
433462
)
434463
.await?;
435-
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
464+
}
465+
if target.is_none() && !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version()
466+
{
436467
// This is a Delta Chat MDN. Mark as read.
437-
markseen_on_imap_table(context, rfc724_mid).await?;
468+
markseen_on_imap_table(context, rfc724_mid_orig).await?;
438469
}
439470
}
440471

@@ -528,6 +559,10 @@ async fn add_parts(
528559
prevent_rename: bool,
529560
verified_encryption: VerifiedEncryption,
530561
) -> Result<ReceivedMsg> {
562+
let rfc724_mid_orig = &mime_parser
563+
.get_rfc724_mid()
564+
.unwrap_or(rfc724_mid.to_string());
565+
531566
let mut chat_id = None;
532567
let mut chat_id_blocked = Blocked::Not;
533568

@@ -1308,7 +1343,7 @@ RETURNING id
13081343
"#)?;
13091344
let row_id: MsgId = stmt.query_row(params![
13101345
replace_msg_id,
1311-
rfc724_mid,
1346+
rfc724_mid_orig,
13121347
if trash { DC_CHAT_ID_TRASH } else { chat_id },
13131348
if trash { ContactId::UNDEFINED } else { from_id },
13141349
if trash { ContactId::UNDEFINED } else { to_id },

src/receive_imf/tests.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3223,6 +3223,22 @@ async fn test_thunderbird_unsigned_with_unencrypted_subject() -> Result<()> {
32233223
Ok(())
32243224
}
32253225

3226+
/// Tests that DC takes the correct Message-ID from the encrypted message part, not the unencrypted
3227+
/// one messed up by the server.
3228+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3229+
async fn test_messed_up_message_id() -> Result<()> {
3230+
let t = TestContext::new_bob().await;
3231+
3232+
let raw = include_bytes!("../../test-data/message/messed_up_message_id.eml");
3233+
receive_imf(&t, raw, false).await?;
3234+
assert_eq!(
3235+
t.get_last_msg().await.rfc724_mid,
3236+
"0bb9ffe1-2596-d997-95b4-1fef8cc4808e@example.org"
3237+
);
3238+
3239+
Ok(())
3240+
}
3241+
32263242
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
32273243
async fn test_mua_user_adds_member() -> Result<()> {
32283244
let t = TestContext::new_alice().await;

0 commit comments

Comments
 (0)