Skip to content

Commit 5663fff

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. To avoid triggering IMAP move loop described in the comments we now only move the messages from INBOX and Spam folders.
1 parent 7a65541 commit 5663fff

File tree

14 files changed

+80
-150
lines changed

14 files changed

+80
-150
lines changed

python/tests/test_1_online.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ def test_move_works(acfactory):
333333

334334

335335
def test_move_avoids_loop(acfactory):
336-
"""Test that the message is only moved once.
336+
"""Test that the message is only moved from INBOX to DeltaChat.
337337
338338
This is to avoid busy loop if moved message reappears in the Inbox
339339
or some scanned folder later.
@@ -344,27 +344,43 @@ def test_move_avoids_loop(acfactory):
344344
ac1 = acfactory.new_online_configuring_account()
345345
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
346346
acfactory.bring_accounts_online()
347+
348+
# Create INBOX.DeltaChat folder and make sure
349+
# it is detected by full folder scan.
350+
ac2.direct_imap.create_folder("INBOX.DeltaChat")
351+
ac2.stop_io()
352+
ac2.start_io()
353+
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
354+
347355
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
348356
ac1_chat.send_text("Message 1")
349357

350358
# Message is moved to the DeltaChat folder and downloaded.
351359
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
352360
assert ac2_msg1.text == "Message 1"
353361

354-
# Move the message to the INBOX again.
362+
# Move the message to the INBOX.DeltaChat again.
363+
# We assume that test server uses "." as the delimiter.
355364
ac2.direct_imap.select_folder("DeltaChat")
356-
ac2.direct_imap.conn.move(["*"], "INBOX")
365+
ac2.direct_imap.conn.move(["*"], "INBOX.DeltaChat")
357366

358367
ac1_chat.send_text("Message 2")
359368
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
360369
assert ac2_msg2.text == "Message 2"
361370

362-
# Check that Message 1 is still in the INBOX folder
371+
# Stop and start I/O to trigger folder scan.
372+
ac2.stop_io()
373+
ac2.start_io()
374+
ac2._evtracker.get_info_contains("Found folders:") # Wait until the end of folder scan.
375+
376+
# Check that Message 1 is still in the INBOX.DeltaChat folder
363377
# and Message 2 is in the DeltaChat folder.
364378
ac2.direct_imap.select_folder("INBOX")
365-
assert len(ac2.direct_imap.get_all_messages()) == 1
379+
assert len(ac2.direct_imap.get_all_messages()) == 0
366380
ac2.direct_imap.select_folder("DeltaChat")
367381
assert len(ac2.direct_imap.get_all_messages()) == 1
382+
ac2.direct_imap.select_folder("INBOX.DeltaChat")
383+
assert len(ac2.direct_imap.get_all_messages()) == 1
368384

369385

370386
def test_move_works_on_self_sent(acfactory):
@@ -471,14 +487,19 @@ def test_resend_message(acfactory, lp):
471487
lp.sec("ac2: receive message")
472488
msg_in = ac2._evtracker.wait_next_incoming_message()
473489
assert msg_in.text == "message"
474-
chat2 = msg_in.chat
475-
chat2_msg_cnt = len(chat2.get_messages())
476490

477491
lp.sec("ac1: resend message")
478492
ac1.resend_messages([msg_in])
479493

480-
lp.sec("ac2: check that message is deleted")
481-
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
494+
lp.sec("ac1: send another message")
495+
chat1.send_text("another message")
496+
497+
lp.sec("ac2: receive another message")
498+
msg_in = ac2._evtracker.wait_next_incoming_message()
499+
assert msg_in.text == "another message"
500+
chat2 = msg_in.chat
501+
chat2_msg_cnt = len(chat2.get_messages())
502+
482503
assert len(chat2.get_messages()) == chat2_msg_cnt
483504

484505

src/config.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -735,9 +735,6 @@ impl Context {
735735
_ => Default::default(),
736736
};
737737
self.set_config_internal(key, value).await?;
738-
if key == Config::SentboxWatch {
739-
self.last_full_folder_scan.lock().await.take();
740-
}
741738
Ok(())
742739
}
743740

src/context.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,6 @@ pub struct InnerContext {
263263
/// IMAP METADATA.
264264
pub(crate) metadata: RwLock<Option<ServerMetadata>>,
265265

266-
pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
267-
268266
/// ID for this `Context` in the current process.
269267
///
270268
/// This allows for multiple `Context`s open in a single process where each context can
@@ -445,7 +443,6 @@ impl Context {
445443
server_id: RwLock::new(None),
446444
metadata: RwLock::new(None),
447445
creation_time: tools::Time::now(),
448-
last_full_folder_scan: Mutex::new(None),
449446
last_error: parking_lot::RwLock::new("".to_string()),
450447
debug_logging: std::sync::RwLock::new(None),
451448
push_subscriber,

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: 9 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -581,52 +581,26 @@ impl Imap {
581581

582582
// Determine the target folder where the message should be moved to.
583583
//
584-
// If we have seen the message on the IMAP server before, do not move it.
584+
// We only move the messages from the INBOX folder.
585585
// This is required to avoid infinite MOVE loop on IMAP servers
586586
// that alias `DeltaChat` folder to other names.
587587
// For example, some Dovecot servers alias `DeltaChat` folder to `INBOX.DeltaChat`.
588-
// In this case Delta Chat configured with `DeltaChat` as the destination folder
589-
// would detect messages in the `INBOX.DeltaChat` folder
590-
// and try to move them to the `DeltaChat` folder.
591-
// Such move to the same folder results in the messages
592-
// getting a new UID, so the messages will be detected as new
588+
// In this case moving from `INBOX.DeltaChat` to `DeltaChat`
589+
// results in the messages getting a new UID,
590+
// so the messages will be detected as new
593591
// in the `INBOX.DeltaChat` folder again.
594592
let _target;
595593
let target = if let Some(message_id) = &message_id {
596594
let msg_info =
597595
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
598-
let delete = if let Some((_, _, true)) = msg_info {
596+
let delete = if let Some((_, true)) = msg_info {
599597
info!(context, "Deleting locally deleted message {message_id}.");
600598
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
612599
} else {
613600
false
614601
};
615602
if delete {
616603
&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
630604
} else {
631605
_target = target_folder(context, folder, folder_meaning, &headers).await?;
632606
&_target
@@ -707,7 +681,6 @@ impl Imap {
707681
.fetch_many_msgs(
708682
context,
709683
folder,
710-
uid_validity,
711684
uids_fetch_in_batch.split_off(0),
712685
&uid_message_ids,
713686
fetch_partially,
@@ -1305,7 +1278,6 @@ impl Session {
13051278
&mut self,
13061279
context: &Context,
13071280
folder: &str,
1308-
uidvalidity: u32,
13091281
request_uids: Vec<u32>,
13101282
uid_message_ids: &BTreeMap<u32, String>,
13111283
fetch_partially: bool,
@@ -1433,18 +1405,7 @@ impl Session {
14331405
context,
14341406
"Passing message UID {} to receive_imf().", request_uid
14351407
);
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-
{
1408+
match receive_imf_inner(context, rfc724_mid, body, is_seen, partial).await {
14481409
Ok(received_msg) => {
14491410
if let Some(m) = received_msg {
14501411
received_msgs.push(m);
@@ -1952,7 +1913,9 @@ pub async fn target_folder_cfg(
19521913

19531914
if folder_meaning == FolderMeaning::Spam {
19541915
spam_target_folder_cfg(context, headers).await
1955-
} else if needs_move_to_mvbox(context, headers).await? {
1916+
} else if folder_meaning == FolderMeaning::Inbox
1917+
&& needs_move_to_mvbox(context, headers).await?
1918+
{
19561919
Ok(Some(Config::ConfiguredMvboxFolder))
19571920
} else {
19581921
Ok(None)
@@ -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/imap/scan_folders.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ impl Imap {
1818
) -> Result<bool> {
1919
// First of all, debounce to once per minute:
2020
{
21-
let mut last_scan = context.last_full_folder_scan.lock().await;
21+
let mut last_scan = session.last_full_folder_scan.lock().await;
2222
if let Some(last_scan) = *last_scan {
2323
let elapsed_secs = time_elapsed(&last_scan).as_secs();
2424
let debounce_secs = context

src/imap/session.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ use anyhow::{Context as _, Result};
55
use async_imap::types::Mailbox;
66
use async_imap::Session as ImapSession;
77
use futures::TryStreamExt;
8+
use tokio::sync::Mutex;
89

910
use crate::imap::capabilities::Capabilities;
1011
use crate::net::session::SessionStream;
12+
use crate::tools;
1113

1214
/// Prefetch:
1315
/// - Message-ID to check if we already have the message.
@@ -40,6 +42,8 @@ pub(crate) struct Session {
4042

4143
pub selected_folder_needs_expunge: bool,
4244

45+
pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
46+
4347
/// True if currently selected folder has new messages.
4448
///
4549
/// Should be false if no folder is currently selected.
@@ -71,6 +75,7 @@ impl Session {
7175
selected_folder: None,
7276
selected_mailbox: None,
7377
selected_folder_needs_expunge: false,
78+
last_full_folder_scan: Mutex::new(None),
7479
new_mail: false,
7580
}
7681
}

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"

0 commit comments

Comments
 (0)