diff --git a/src/imap/mod.rs b/src/imap/mod.rs index d9950feeb..409e65c01 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -108,6 +108,7 @@ const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\ AUTOCRYPT-SETUP-MESSAGE\ )])"; const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])"; +const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])"; const JUST_UID: &str = "(UID)"; const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])"; const SELECT_ALL: &str = "1:*"; @@ -517,6 +518,84 @@ impl Imap { } } + /// Synchronizes UIDs in the database with UIDs on the server. + /// + /// It is assumed that no operations are taking place on the same + /// folder at the moment. Make sure to run it in the same + /// thread/task as other network operations on this folder to + /// avoid race conditions. + pub(crate) async fn resync_folder_uids( + &mut self, + context: &Context, + folder: String, + ) -> Result<()> { + // Collect pairs of UID and Message-ID. + let mut msg_ids = BTreeMap::new(); + + self.select_folder(context, Some(&folder)).await?; + + let session = if let Some(ref mut session) = &mut self.session { + session + } else { + return Err(Error::NoConnection); + }; + + match session.uid_fetch("1:*", RFC724MID_UID).await { + Ok(mut list) => { + while let Some(fetch) = list.next().await { + let msg = fetch.map_err(|err| Error::Other(err.to_string()))?; + + // Get Message-ID + let message_id = get_fetch_headers(&msg) + .and_then(|headers| prefetch_get_message_id(&headers)) + .ok(); + + if let (Some(uid), Some(rfc724_mid)) = (msg.uid, message_id) { + msg_ids.insert(uid, rfc724_mid); + } + } + } + Err(err) => { + return Err(Error::Other(format!( + "Can't resync folder {}: {}", + folder, err + ))) + } + } + + info!( + context, + "Resync: collected {} message IDs in folder {}", + msg_ids.len(), + &folder + ); + + // Write collected UIDs to SQLite database. + context + .sql + .with_conn(move |mut conn| { + let conn2 = &mut conn; + let tx = conn2.transaction()?; + tx.execute( + "UPDATE msgs SET server_uid=0 WHERE server_folder=?", + params![folder], + )?; + for (uid, rfc724_mid) in &msg_ids { + // This may detect previously undetected moved + // messages, so we update server_folder too. + tx.execute( + "UPDATE msgs \ + SET server_folder=?,server_uid=? WHERE rfc724_mid=?", + params![folder, uid, rfc724_mid], + )?; + } + tx.commit()?; + Ok(()) + }) + .await?; + Ok(()) + } + /// return Result with (uid_validity, last_seen_uid) tuple. pub(crate) async fn select_with_uidvalidity( &mut self, diff --git a/src/job.rs b/src/job.rs index 26e078977..12c427d94 100644 --- a/src/job.rs +++ b/src/job.rs @@ -102,6 +102,10 @@ pub enum Action { MoveMsg = 200, DeleteMsgOnImap = 210, + // UID synchronization is high-priority to make sure correct UIDs + // are used by message moving/deletion. + ResyncFolders = 300, + // Jobs in the SMTP-thread, range from DC_SMTP_THREAD..DC_SMTP_THREAD+999 MaybeSendLocations = 5005, // low priority ... MaybeSendLocationsEnded = 5007, @@ -124,6 +128,7 @@ impl From for Thread { Housekeeping => Thread::Imap, DeleteMsgOnImap => Thread::Imap, + ResyncFolders => Thread::Imap, EmptyServer => Thread::Imap, MarkseenMsgOnImap => Thread::Imap, MoveMsg => Thread::Imap, @@ -619,6 +624,44 @@ impl Job { } } + /// Synchronizes UIDs for sentbox, inbox and mvbox, in this order. + /// + /// If a copy of the message is present in multiple folders, mvbox + /// is preferred to inbox, which is in turn preferred to + /// sentbox. This is because in the database it is impossible to + /// store multiple UIDs for one message, so we prefer to + /// automatically delete messages in the folders managed by Delta + /// Chat in contrast to the Sent folder, which is normally managed + /// by the user via webmail or another email client. + async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status { + if let Err(err) = imap.connect_configured(context).await { + warn!(context, "could not connect: {:?}", err); + return Status::RetryLater; + } + + if let Some(sentbox_folder) = &context.get_config(Config::ConfiguredSentboxFolder).await { + job_try!( + imap.resync_folder_uids(context, sentbox_folder.to_string()) + .await + ); + } + + if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await { + job_try!( + imap.resync_folder_uids(context, inbox_folder.to_string()) + .await + ); + } + + if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await { + job_try!( + imap.resync_folder_uids(context, mvbox_folder.to_string()) + .await + ); + } + Status::Finished(Ok(())) + } + async fn empty_server(&mut self, context: &Context, imap: &mut Imap) -> Status { if let Err(err) = imap.connect_configured(context).await { warn!(context, "could not connect: {:?}", err); @@ -986,6 +1029,7 @@ async fn perform_job_action( } Action::EmptyServer => job.empty_server(context, connection.inbox()).await, Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await, + Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await, Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await, Action::MoveMsg => job.move_msg(context, connection.inbox()).await, Action::Housekeeping => { @@ -1043,6 +1087,7 @@ pub async fn add(context: &Context, job: Job) { Action::Housekeeping | Action::EmptyServer | Action::DeleteMsgOnImap + | Action::ResyncFolders | Action::MarkseenMsgOnImap | Action::MoveMsg => { info!(context, "interrupt: imap");