Skip to content

Commit caf6eff

Browse files
committed
feat: protect the Date header
We do not try to delete resent messages anymore. Previously resent messages were distinguised by having duplicate Message-ID, but future Date, but now we need to download the message before we even see the Date. We now move the message to the destination folder but do not fetch it. It may not be a good idea to delete the duplicate in multi-device setups anyway, because the device which has a message may delete the duplicate of a message the other device missed.
1 parent 37dc1f5 commit caf6eff

File tree

9 files changed

+44
-125
lines changed

9 files changed

+44
-125
lines changed

python/tests/test_1_online.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -471,14 +471,19 @@ def test_resend_message(acfactory, lp):
471471
lp.sec("ac2: receive message")
472472
msg_in = ac2._evtracker.wait_next_incoming_message()
473473
assert msg_in.text == "message"
474-
chat2 = msg_in.chat
475-
chat2_msg_cnt = len(chat2.get_messages())
476474

477475
lp.sec("ac1: resend message")
478476
ac1.resend_messages([msg_in])
479477

480-
lp.sec("ac2: check that message is deleted")
481-
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
478+
lp.sec("ac1: send another message")
479+
chat1.send_text("another message")
480+
481+
lp.sec("ac2: receive another message")
482+
msg_in = ac2._evtracker.wait_next_incoming_message()
483+
assert msg_in.text == "another message"
484+
chat2 = msg_in.chat
485+
chat2_msg_cnt = len(chat2.get_messages())
486+
482487
assert len(chat2.get_messages()) == chat2_msg_cnt
483488

484489

src/download.rs

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,25 +161,18 @@ pub(crate) async fn download_msg(
161161
|row| {
162162
let server_uid: u32 = row.get(0)?;
163163
let server_folder: String = row.get(1)?;
164-
let uidvalidity: u32 = row.get(2)?;
165-
Ok((server_uid, server_folder, uidvalidity))
164+
Ok((server_uid, server_folder))
166165
},
167166
)
168167
.await?;
169168

170-
let Some((server_uid, server_folder, uidvalidity)) = row else {
169+
let Some((server_uid, server_folder)) = row else {
171170
// No IMAP record found, we don't know the UID and folder.
172171
return Err(anyhow!("Call download_full() again to try over."));
173172
};
174173

175174
session
176-
.fetch_single_msg(
177-
context,
178-
&server_folder,
179-
uidvalidity,
180-
server_uid,
181-
msg.rfc724_mid.clone(),
182-
)
175+
.fetch_single_msg(context, &server_folder, server_uid, msg.rfc724_mid.clone())
183176
.await?;
184177
Ok(())
185178
}
@@ -193,7 +186,6 @@ impl Session {
193186
&mut self,
194187
context: &Context,
195188
folder: &str,
196-
uidvalidity: u32,
197189
uid: u32,
198190
rfc724_mid: String,
199191
) -> Result<()> {
@@ -213,14 +205,7 @@ impl Session {
213205
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
214206
uid_message_ids.insert(uid, rfc724_mid);
215207
let (last_uid, _received) = self
216-
.fetch_many_msgs(
217-
context,
218-
folder,
219-
uidvalidity,
220-
vec![uid],
221-
&uid_message_ids,
222-
false,
223-
)
208+
.fetch_many_msgs(context, folder, vec![uid], &uid_message_ids, false)
224209
.await?;
225210
if last_uid.is_none() {
226211
bail!("Failed to fetch UID {uid}");

src/imap.rs

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -595,38 +595,14 @@ impl Imap {
595595
let target = if let Some(message_id) = &message_id {
596596
let msg_info =
597597
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
598-
let delete = if let Some((_, _, true)) = msg_info {
598+
let delete = if let Some((_, true)) = msg_info {
599599
info!(context, "Deleting locally deleted message {message_id}.");
600600
true
601-
} else if let Some((_, ts_sent_old, _)) = msg_info {
602-
let is_chat_msg = headers.get_header_value(HeaderDef::ChatVersion).is_some();
603-
let ts_sent = headers
604-
.get_header_value(HeaderDef::Date)
605-
.and_then(|v| mailparse::dateparse(&v).ok())
606-
.unwrap_or_default();
607-
let is_dup = is_dup_msg(is_chat_msg, ts_sent, ts_sent_old);
608-
if is_dup {
609-
info!(context, "Deleting duplicate message {message_id}.");
610-
}
611-
is_dup
612601
} else {
613602
false
614603
};
615604
if delete {
616605
&delete_target
617-
} else if context
618-
.sql
619-
.exists(
620-
"SELECT COUNT (*) FROM imap WHERE rfc724_mid=?",
621-
(message_id,),
622-
)
623-
.await?
624-
{
625-
info!(
626-
context,
627-
"Not moving the message {} that we have seen before.", &message_id
628-
);
629-
folder
630606
} else {
631607
_target = target_folder(context, folder, folder_meaning, &headers).await?;
632608
&_target
@@ -707,7 +683,6 @@ impl Imap {
707683
.fetch_many_msgs(
708684
context,
709685
folder,
710-
uid_validity,
711686
uids_fetch_in_batch.split_off(0),
712687
&uid_message_ids,
713688
fetch_partially,
@@ -1305,7 +1280,6 @@ impl Session {
13051280
&mut self,
13061281
context: &Context,
13071282
folder: &str,
1308-
uidvalidity: u32,
13091283
request_uids: Vec<u32>,
13101284
uid_message_ids: &BTreeMap<u32, String>,
13111285
fetch_partially: bool,
@@ -1433,18 +1407,7 @@ impl Session {
14331407
context,
14341408
"Passing message UID {} to receive_imf().", request_uid
14351409
);
1436-
match receive_imf_inner(
1437-
context,
1438-
folder,
1439-
uidvalidity,
1440-
request_uid,
1441-
rfc724_mid,
1442-
body,
1443-
is_seen,
1444-
partial,
1445-
)
1446-
.await
1447-
{
1410+
match receive_imf_inner(context, rfc724_mid, body, is_seen, partial).await {
14481411
Ok(received_msg) => {
14491412
if let Some(m) = received_msg {
14501413
received_msgs.push(m);
@@ -2280,15 +2243,6 @@ pub(crate) async fn prefetch_should_download(
22802243
Ok(should_download)
22812244
}
22822245

2283-
/// Returns whether a message is a duplicate (resent message).
2284-
pub(crate) fn is_dup_msg(is_chat_msg: bool, ts_sent: i64, ts_sent_old: i64) -> bool {
2285-
// If the existing message has timestamp_sent == 0, that means we don't know its actual sent
2286-
// timestamp, so don't delete the new message. E.g. outgoing messages have zero timestamp_sent
2287-
// because they are stored to the db before sending. Also consider as duplicates only messages
2288-
// with greater timestamp to avoid deleting both messages in a multi-device setting.
2289-
is_chat_msg && ts_sent_old != 0 && ts_sent > ts_sent_old
2290-
}
2291-
22922246
/// Marks messages in `msgs` table as seen, searching for them by UID.
22932247
///
22942248
/// Returns updated chat ID if any message was marked as seen.

src/message.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,7 +1319,7 @@ impl Message {
13191319
/// `References` header is not taken into account.
13201320
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
13211321
if let Some(in_reply_to) = &self.in_reply_to {
1322-
if let Some((msg_id, _ts_sent)) = rfc724_mid_exists(context, in_reply_to).await? {
1322+
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
13231323
let msg = Message::load_from_db_optional(context, msg_id).await?;
13241324
return Ok(msg);
13251325
}
@@ -2139,13 +2139,13 @@ pub async fn estimate_deletion_cnt(
21392139
pub(crate) async fn rfc724_mid_exists(
21402140
context: &Context,
21412141
rfc724_mid: &str,
2142-
) -> Result<Option<(MsgId, i64)>> {
2142+
) -> Result<Option<MsgId>> {
21432143
Ok(rfc724_mid_exists_ex(context, rfc724_mid, "1")
21442144
.await?
2145-
.map(|(id, ts_sent, _)| (id, ts_sent)))
2145+
.map(|(id, _)| id))
21462146
}
21472147

2148-
/// Returns [MsgId] and "sent" timestamp of the most recent message with given `rfc724_mid`
2148+
/// Returns [MsgId] of the most recent message with given `rfc724_mid`
21492149
/// (Message-ID header) and bool `expr` result if such messages exists in the db.
21502150
///
21512151
/// * `expr`: SQL expression additionally passed into `SELECT`. Evaluated to `true` iff it is true
@@ -2154,7 +2154,7 @@ pub(crate) async fn rfc724_mid_exists_ex(
21542154
context: &Context,
21552155
rfc724_mid: &str,
21562156
expr: &str,
2157-
) -> Result<Option<(MsgId, i64, bool)>> {
2157+
) -> Result<Option<(MsgId, bool)>> {
21582158
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
21592159
if rfc724_mid.is_empty() {
21602160
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
@@ -2172,9 +2172,8 @@ pub(crate) async fn rfc724_mid_exists_ex(
21722172
(rfc724_mid,),
21732173
|row| {
21742174
let msg_id: MsgId = row.get(0)?;
2175-
let timestamp_sent: i64 = row.get(1)?;
21762175
let expr_res: bool = row.get(2)?;
2177-
Ok((msg_id, timestamp_sent, expr_res))
2176+
Ok((msg_id, expr_res))
21782177
},
21792178
)
21802179
.await?;
@@ -2194,7 +2193,7 @@ pub(crate) async fn get_by_rfc724_mids(
21942193
) -> Result<Option<Message>> {
21952194
let mut latest = None;
21962195
for id in mids.iter().rev() {
2197-
let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? else {
2196+
let Some(msg_id) = rfc724_mid_exists(context, id).await? else {
21982197
continue;
21992198
};
22002199
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {

src/mimefactory.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,14 @@ impl MimeFactory {
871871
} else {
872872
unprotected_headers.push(header.clone());
873873
}
874+
} else if is_encrypted && header_name == "date" {
875+
protected_headers.push(header.clone());
876+
unprotected_headers.push((
877+
"Date",
878+
mail_builder::headers::HeaderType::Raw(
879+
"Thu, 01 Jan 1970 00:00:00 +0000".into(),
880+
),
881+
));
874882
} else if is_encrypted {
875883
protected_headers.push(header.clone());
876884

@@ -881,8 +889,7 @@ impl MimeFactory {
881889
mail_builder::headers::raw::Raw::new("[...]").into(),
882890
));
883891
}
884-
"date"
885-
| "in-reply-to"
892+
"in-reply-to"
886893
| "references"
887894
| "auto-submitted"
888895
| "chat-version"

src/reaction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ pub(crate) async fn set_msg_reaction(
277277
reaction: Reaction,
278278
is_incoming_fresh: bool,
279279
) -> Result<()> {
280-
if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
280+
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
281281
set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
282282

283283
if is_incoming_fresh

src/receive_imf.rs

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::aheader::EncryptPreference;
1616
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
1717
use crate::config::Config;
1818
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH, EDITED_PREFIX};
19+
use crate::contact;
1920
use crate::contact::{Contact, ContactId, Origin};
2021
use crate::context::Context;
2122
use crate::debug_logging::maybe_set_logging_xdc_inner;
@@ -41,7 +42,6 @@ use crate::stock_str;
4142
use crate::sync::Sync::*;
4243
use crate::tools::{self, buf_compress, remove_subject_prefix};
4344
use crate::{chatlist_events, location};
44-
use crate::{contact, imap};
4545

4646
/// This is the struct that is returned after receiving one email (aka MIME message).
4747
///
@@ -85,7 +85,7 @@ pub async fn receive_imf(
8585
) -> Result<Option<ReceivedMsg>> {
8686
let mail = mailparse::parse_mail(imf_raw).context("can't parse mail")?;
8787
let rfc724_mid =
88-
imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
88+
crate::imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
8989
if let Some(download_limit) = context.download_limit().await? {
9090
let download_limit: usize = download_limit.try_into()?;
9191
if imf_raw.len() > download_limit {
@@ -117,17 +117,7 @@ pub(crate) async fn receive_imf_from_inbox(
117117
seen: bool,
118118
is_partial_download: Option<u32>,
119119
) -> Result<Option<ReceivedMsg>> {
120-
receive_imf_inner(
121-
context,
122-
"INBOX",
123-
0,
124-
0,
125-
rfc724_mid,
126-
imf_raw,
127-
seen,
128-
is_partial_download,
129-
)
130-
.await
120+
receive_imf_inner(context, rfc724_mid, imf_raw, seen, is_partial_download).await
131121
}
132122

133123
/// Inserts a tombstone into `msgs` table
@@ -159,12 +149,8 @@ async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId>
159149
/// If `is_partial_download` is set, it contains the full message size in bytes.
160150
/// Do not confuse that with `replace_msg_id` that will be set when the full message is loaded
161151
/// later.
162-
#[expect(clippy::too_many_arguments)]
163152
pub(crate) async fn receive_imf_inner(
164153
context: &Context,
165-
folder: &str,
166-
uidvalidity: u32,
167-
uid: u32,
168154
rfc724_mid: &str,
169155
imf_raw: &[u8],
170156
seen: bool,
@@ -228,7 +214,7 @@ pub(crate) async fn receive_imf_inner(
228214
// check, if the mail is already in our database.
229215
// make sure, this check is done eg. before securejoin-processing.
230216
let (replace_msg_id, replace_chat_id);
231-
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
217+
if let Some(old_msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
232218
if is_partial_download.is_some() {
233219
// Should never happen, see imap::prefetch_should_download(), but still.
234220
info!(
@@ -252,24 +238,9 @@ pub(crate) async fn receive_imf_inner(
252238
} else {
253239
replace_msg_id = if rfc724_mid_orig == rfc724_mid {
254240
None
255-
} else if let Some((old_msg_id, old_ts_sent)) =
241+
} else if let Some(old_msg_id) =
256242
message::rfc724_mid_exists(context, rfc724_mid_orig).await?
257243
{
258-
if imap::is_dup_msg(
259-
mime_parser.has_chat_version(),
260-
mime_parser.timestamp_sent,
261-
old_ts_sent,
262-
) {
263-
info!(context, "Deleting duplicate message {rfc724_mid_orig}.");
264-
let target = context.get_delete_msgs_target().await?;
265-
context
266-
.sql
267-
.execute(
268-
"UPDATE imap SET target=? WHERE folder=? AND uidvalidity=? AND uid=?",
269-
(target, folder, uidvalidity, uid),
270-
)
271-
.await?;
272-
}
273244
Some(old_msg_id)
274245
} else {
275246
None
@@ -1466,7 +1437,7 @@ async fn add_parts(
14661437
chat_id = DC_CHAT_ID_TRASH;
14671438
match mime_parser.get_header(HeaderDef::InReplyTo) {
14681439
Some(in_reply_to) => match rfc724_mid_exists(context, in_reply_to).await? {
1469-
Some((instance_id, _ts_sent)) => {
1440+
Some(instance_id) => {
14701441
if let Err(err) =
14711442
add_gossip_peer_from_header(context, instance_id, node_addr).await
14721443
{
@@ -1766,7 +1737,7 @@ async fn handle_edit_delete(
17661737
from_id: ContactId,
17671738
) -> Result<bool> {
17681739
if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
1769-
if let Some((original_msg_id, _)) = rfc724_mid_exists(context, rfc724_mid).await? {
1740+
if let Some(original_msg_id) = rfc724_mid_exists(context, rfc724_mid).await? {
17701741
if let Some(mut original_msg) =
17711742
Message::load_from_db_optional(context, original_msg_id).await?
17721743
{
@@ -1809,9 +1780,7 @@ async fn handle_edit_delete(
18091780

18101781
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
18111782
for rfc724_mid in rfc724_mid_vec {
1812-
if let Some((msg_id, _)) =
1813-
message::rfc724_mid_exists(context, rfc724_mid).await?
1814-
{
1783+
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
18151784
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
18161785
if msg.from_id == from_id {
18171786
message::delete_msg_locally(context, &msg).await?;
@@ -3184,7 +3153,7 @@ async fn get_previous_message(
31843153
) -> Result<Option<Message>> {
31853154
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
31863155
if let Some(rfc724mid) = parse_message_ids(field).last() {
3187-
if let Some((msg_id, _)) = rfc724_mid_exists(context, rfc724mid).await? {
3156+
if let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await? {
31883157
return Message::load_from_db_optional(context, msg_id).await;
31893158
}
31903159
}

src/receive_imf/receive_imf_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1973,7 +1973,7 @@ async fn check_alias_reply(from_dc: bool, chat_request: bool, group_request: boo
19731973
.await
19741974
.unwrap();
19751975

1976-
let (msg_id, _) = rfc724_mid_exists(&claire, "non-dc-1@example.org")
1976+
let msg_id = rfc724_mid_exists(&claire, "non-dc-1@example.org")
19771977
.await
19781978
.unwrap()
19791979
.unwrap();

0 commit comments

Comments
 (0)