1use std::{
7 cmp::max,
8 cmp::min,
9 collections::{BTreeMap, BTreeSet, HashMap},
10 iter::Peekable,
11 mem::take,
12 sync::atomic::Ordering,
13 time::{Duration, UNIX_EPOCH},
14};
15
16use anyhow::{bail, ensure, format_err, Context as _, Result};
17use async_channel::Receiver;
18use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
19use deltachat_contact_tools::ContactAddress;
20use futures::{FutureExt as _, StreamExt, TryStreamExt};
21use futures_lite::FutureExt;
22use num_traits::FromPrimitive;
23use rand::Rng;
24use ratelimit::Ratelimit;
25use url::Url;
26
27use crate::chat::{self, ChatId, ChatIdBlocked};
28use crate::chatlist_events;
29use crate::config::Config;
30use crate::constants::{self, Blocked, Chattype, ShowEmails};
31use crate::contact::{Contact, ContactId, Modifier, Origin};
32use crate::context::Context;
33use crate::events::EventType;
34use crate::headerdef::{HeaderDef, HeaderDefMap};
35use crate::log::LogExt;
36use crate::login_param::{
37 prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
38};
39use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
40use crate::mimeparser;
41use crate::net::proxy::ProxyConfig;
42use crate::net::session::SessionStream;
43use crate::oauth2::get_oauth2_access_token;
44use crate::push::encrypt_device_token;
45use crate::receive_imf::{
46 from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
47};
48use crate::scheduler::connectivity::ConnectivityStore;
49use crate::stock_str;
50use crate::tools::{self, create_id, duration_to_str};
51
52pub(crate) mod capabilities;
53mod client;
54mod idle;
55pub mod scan_folders;
56pub mod select_folder;
57pub(crate) mod session;
58
59use client::{determine_capabilities, Client};
60use mailparse::SingleInfo;
61use session::Session;
62
63pub(crate) const GENERATED_PREFIX: &str = "GEN_";
64
65const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
66 MESSAGE-ID \
67 X-MICROSOFT-ORIGINAL-MESSAGE-ID\
68 )])";
69const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
70const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
71
72#[derive(Debug)]
73pub(crate) struct Imap {
74 pub(crate) idle_interrupt_receiver: Receiver<()>,
75
76 addr: String,
78
79 lp: Vec<ConfiguredServerLoginParam>,
81
82 password: String,
84
85 proxy_config: Option<ProxyConfig>,
87
88 strict_tls: bool,
89
90 oauth2: bool,
91
92 authentication_failed_once: bool,
93
94 pub(crate) connectivity: ConnectivityStore,
95
96 conn_last_try: tools::Time,
97 conn_backoff_ms: u64,
98
99 ratelimit: Ratelimit,
107}
108
109#[derive(Debug)]
110struct OAuth2 {
111 user: String,
112 access_token: String,
113}
114
115#[derive(Debug)]
116pub(crate) struct ServerMetadata {
117 pub comment: Option<String>,
120
121 pub admin: Option<String>,
124
125 pub iroh_relay: Option<Url>,
126}
127
128impl async_imap::Authenticator for OAuth2 {
129 type Response = String;
130
131 fn process(&mut self, _data: &[u8]) -> Self::Response {
132 format!(
133 "user={}\x01auth=Bearer {}\x01\x01",
134 self.user, self.access_token
135 )
136 }
137}
138
139#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
140pub enum FolderMeaning {
141 Unknown,
142
143 Spam,
145 Inbox,
146 Mvbox,
147 Sent,
148 Trash,
149 Drafts,
150
151 Virtual,
158}
159
160impl FolderMeaning {
161 pub fn to_config(self) -> Option<Config> {
162 match self {
163 FolderMeaning::Unknown => None,
164 FolderMeaning::Spam => None,
165 FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
166 FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
167 FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
168 FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
169 FolderMeaning::Drafts => None,
170 FolderMeaning::Virtual => None,
171 }
172 }
173}
174
175struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
176 inner: Peekable<T>,
177}
178
179impl<T, I> From<I> for UidGrouper<T>
180where
181 T: Iterator<Item = (i64, u32, String)>,
182 I: IntoIterator<IntoIter = T>,
183{
184 fn from(inner: I) -> Self {
185 Self {
186 inner: inner.into_iter().peekable(),
187 }
188 }
189}
190
191impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
192 type Item = (String, Vec<i64>, String);
194
195 fn next(&mut self) -> Option<Self::Item> {
196 let (_, _, folder) = self.inner.peek().cloned()?;
197
198 let mut uid_set = String::new();
199 let mut rowid_set = Vec::new();
200
201 while uid_set.len() < 1000 {
202 if let Some((start_rowid, start_uid, _)) = self
204 .inner
205 .next_if(|(_, _, start_folder)| start_folder == &folder)
206 {
207 rowid_set.push(start_rowid);
208 let mut end_uid = start_uid;
209
210 while let Some((next_rowid, next_uid, _)) =
211 self.inner.next_if(|(_, next_uid, next_folder)| {
212 next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
213 })
214 {
215 end_uid = next_uid;
216 rowid_set.push(next_rowid);
217 }
218
219 let uid_range = UidRange {
220 start: start_uid,
221 end: end_uid,
222 };
223 if !uid_set.is_empty() {
224 uid_set.push(',');
225 }
226 uid_set.push_str(&uid_range.to_string());
227 } else {
228 break;
229 }
230 }
231
232 Some((folder, rowid_set, uid_set))
233 }
234}
235
236impl Imap {
237 pub fn new(
241 lp: Vec<ConfiguredServerLoginParam>,
242 password: String,
243 proxy_config: Option<ProxyConfig>,
244 addr: &str,
245 strict_tls: bool,
246 oauth2: bool,
247 idle_interrupt_receiver: Receiver<()>,
248 ) -> Self {
249 Imap {
250 idle_interrupt_receiver,
251 addr: addr.to_string(),
252 lp,
253 password,
254 proxy_config,
255 strict_tls,
256 oauth2,
257 authentication_failed_once: false,
258 connectivity: Default::default(),
259 conn_last_try: UNIX_EPOCH,
260 conn_backoff_ms: 0,
261 ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
263 }
264 }
265
266 pub async fn new_configured(
268 context: &Context,
269 idle_interrupt_receiver: Receiver<()>,
270 ) -> Result<Self> {
271 let param = ConfiguredLoginParam::load(context)
272 .await?
273 .context("Not configured")?;
274 let proxy_config = ProxyConfig::load(context).await?;
275 let strict_tls = param.strict_tls(proxy_config.is_some());
276 let imap = Self::new(
277 param.imap.clone(),
278 param.imap_password.clone(),
279 proxy_config,
280 ¶m.addr,
281 strict_tls,
282 param.oauth2,
283 idle_interrupt_receiver,
284 );
285 Ok(imap)
286 }
287
288 pub(crate) async fn connect(
296 &mut self,
297 context: &Context,
298 configuring: bool,
299 ) -> Result<Session> {
300 let now = tools::Time::now();
301 let until_can_send = max(
302 min(self.conn_last_try, now)
303 .checked_add(Duration::from_millis(self.conn_backoff_ms))
304 .unwrap_or(now),
305 now,
306 )
307 .duration_since(now)?;
308 let ratelimit_duration = max(until_can_send, self.ratelimit.until_can_send());
309 if !ratelimit_duration.is_zero() {
310 warn!(
311 context,
312 "IMAP got rate limited, waiting for {} until can connect.",
313 duration_to_str(ratelimit_duration),
314 );
315 let interrupted = async {
316 tokio::time::sleep(ratelimit_duration).await;
317 false
318 }
319 .race(self.idle_interrupt_receiver.recv().map(|_| true))
320 .await;
321 if interrupted {
322 info!(
323 context,
324 "Connecting to IMAP without waiting for ratelimit due to interrupt."
325 );
326 }
327 }
328
329 info!(context, "Connecting to IMAP server");
330 self.connectivity.set_connecting(context).await;
331
332 self.conn_last_try = tools::Time::now();
333 const BACKOFF_MIN_MS: u64 = 2000;
334 const BACKOFF_MAX_MS: u64 = 80_000;
335 self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
336 self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(
337 rand::thread_rng().gen_range((self.conn_backoff_ms / 2)..=self.conn_backoff_ms),
338 );
339 self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
340
341 let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
342 let mut first_error = None;
343 for lp in login_params {
344 info!(context, "IMAP trying to connect to {}.", &lp.connection);
345 let connection_candidate = lp.connection.clone();
346 let client = match Client::connect(
347 context,
348 self.proxy_config.clone(),
349 self.strict_tls,
350 connection_candidate,
351 )
352 .await
353 .context("IMAP failed to connect")
354 {
355 Ok(client) => client,
356 Err(err) => {
357 warn!(context, "{err:#}.");
358 first_error.get_or_insert(err);
359 continue;
360 }
361 };
362
363 self.conn_backoff_ms = BACKOFF_MIN_MS;
364 self.ratelimit.send();
365
366 let imap_user: &str = lp.user.as_ref();
367 let imap_pw: &str = &self.password;
368
369 let login_res = if self.oauth2 {
370 info!(context, "Logging into IMAP server with OAuth 2.");
371 let addr: &str = self.addr.as_ref();
372
373 let token = get_oauth2_access_token(context, addr, imap_pw, true)
374 .await?
375 .context("IMAP could not get OAUTH token")?;
376 let auth = OAuth2 {
377 user: imap_user.into(),
378 access_token: token,
379 };
380 client.authenticate("XOAUTH2", auth).await
381 } else {
382 info!(context, "Logging into IMAP server with LOGIN.");
383 client.login(imap_user, imap_pw).await
384 };
385
386 match login_res {
387 Ok(mut session) => {
388 let capabilities = determine_capabilities(&mut session).await?;
389
390 let session = if capabilities.can_compress {
391 info!(context, "Enabling IMAP compression.");
392 let compressed_session = session
393 .compress(|s| {
394 let session_stream: Box<dyn SessionStream> = Box::new(s);
395 session_stream
396 })
397 .await
398 .context("Failed to enable IMAP compression")?;
399 Session::new(compressed_session, capabilities)
400 } else {
401 Session::new(session, capabilities)
402 };
403
404 let mut lock = context.server_id.write().await;
406 lock.clone_from(&session.capabilities.server_id);
407
408 self.authentication_failed_once = false;
409 context.emit_event(EventType::ImapConnected(format!(
410 "IMAP-LOGIN as {}",
411 lp.user
412 )));
413 self.connectivity.set_preparing(context).await;
414 info!(context, "Successfully logged into IMAP server");
415 return Ok(session);
416 }
417
418 Err(err) => {
419 let imap_user = lp.user.to_owned();
420 let message = stock_str::cannot_login(context, &imap_user).await;
421
422 warn!(context, "IMAP failed to login: {err:#}.");
423 first_error.get_or_insert(format_err!("{message} ({err:#})"));
424
425 let _lock = context.wrong_pw_warning_mutex.lock().await;
427 if err.to_string().to_lowercase().contains("authentication") {
428 if self.authentication_failed_once
429 && !configuring
430 && context.get_config_bool(Config::NotifyAboutWrongPw).await?
431 {
432 let mut msg = Message::new_text(message);
433 if let Err(e) = chat::add_device_msg_with_importance(
434 context,
435 None,
436 Some(&mut msg),
437 true,
438 )
439 .await
440 {
441 warn!(context, "Failed to add device message: {e:#}.");
442 } else {
443 context
444 .set_config_internal(Config::NotifyAboutWrongPw, None)
445 .await
446 .log_err(context)
447 .ok();
448 }
449 } else {
450 self.authentication_failed_once = true;
451 }
452 } else {
453 self.authentication_failed_once = false;
454 }
455 }
456 }
457 }
458
459 Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
460 }
461
462 pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
467 let configuring = false;
468 let mut session = match self.connect(context, configuring).await {
469 Ok(session) => session,
470 Err(err) => {
471 self.connectivity.set_err(context, &err).await;
472 return Err(err);
473 }
474 };
475
476 let folders_configured = context
477 .sql
478 .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
479 .await?;
480 if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
481 let is_chatmail = match context.get_config_bool(Config::FixIsChatmail).await? {
482 false => session.is_chatmail(),
483 true => context.get_config_bool(Config::IsChatmail).await?,
484 };
485 let create_mvbox = !is_chatmail || context.get_config_bool(Config::MvboxMove).await?;
486 self.configure_folders(context, &mut session, create_mvbox)
487 .await?;
488 }
489
490 Ok(session)
491 }
492
493 pub async fn fetch_move_delete(
498 &mut self,
499 context: &Context,
500 session: &mut Session,
501 watch_folder: &str,
502 folder_meaning: FolderMeaning,
503 ) -> Result<()> {
504 if !context.sql.is_open().await {
505 bail!("IMAP operation attempted while it is torn down");
507 }
508
509 let msgs_fetched = self
510 .fetch_new_messages(context, session, watch_folder, folder_meaning)
511 .await
512 .context("fetch_new_messages")?;
513 if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
514 context.scheduler.interrupt_ephemeral_task().await;
519 }
520
521 session
522 .move_delete_messages(context, watch_folder)
523 .await
524 .context("move_delete_messages")?;
525
526 Ok(())
527 }
528
529 pub(crate) async fn fetch_new_messages(
533 &mut self,
534 context: &Context,
535 session: &mut Session,
536 folder: &str,
537 folder_meaning: FolderMeaning,
538 ) -> Result<bool> {
539 if should_ignore_folder(context, folder, folder_meaning).await? {
540 info!(context, "Not fetching from {folder:?}.");
541 session.new_mail = false;
542 return Ok(false);
543 }
544
545 let create = false;
546 let folder_exists = session
547 .select_with_uidvalidity(context, folder, create)
548 .await
549 .with_context(|| format!("Failed to select folder {folder:?}"))?;
550 if !folder_exists {
551 return Ok(false);
552 }
553
554 if !session.new_mail {
555 info!(context, "No new emails in folder {folder:?}.");
556 return Ok(false);
557 }
558 session.new_mail = false;
559
560 let uid_validity = get_uidvalidity(context, folder).await?;
561 let old_uid_next = get_uid_next(context, folder).await?;
562
563 let msgs = session.prefetch(old_uid_next).await.context("prefetch")?;
564 let read_cnt = msgs.len();
565
566 let download_limit = context.download_limit().await?;
567 let mut uids_fetch = Vec::<(_, bool )>::with_capacity(msgs.len() + 1);
568 let mut uid_message_ids = BTreeMap::new();
569 let mut largest_uid_skipped = None;
570 let delete_target = context.get_delete_msgs_target().await?;
571
572 for (uid, ref fetch_response) in msgs {
574 let headers = match get_fetch_headers(fetch_response) {
575 Ok(headers) => headers,
576 Err(err) => {
577 warn!(context, "Failed to parse FETCH headers: {err:#}.");
578 continue;
579 }
580 };
581
582 let message_id = prefetch_get_message_id(&headers);
583
584 let _target;
597 let target = if let Some(message_id) = &message_id {
598 let msg_info =
599 message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
600 let delete = if let Some((_, _, true)) = msg_info {
601 info!(context, "Deleting locally deleted message {message_id}.");
602 true
603 } else if let Some((_, ts_sent_old, _)) = msg_info {
604 let is_chat_msg = headers.get_header_value(HeaderDef::ChatVersion).is_some();
605 let ts_sent = headers
606 .get_header_value(HeaderDef::Date)
607 .and_then(|v| mailparse::dateparse(&v).ok())
608 .unwrap_or_default();
609 let is_dup = is_dup_msg(is_chat_msg, ts_sent, ts_sent_old);
610 if is_dup {
611 info!(context, "Deleting duplicate message {message_id}.");
612 }
613 is_dup
614 } else {
615 false
616 };
617 if delete {
618 &delete_target
619 } else if context
620 .sql
621 .exists(
622 "SELECT COUNT (*) FROM imap WHERE rfc724_mid=?",
623 (message_id,),
624 )
625 .await?
626 {
627 info!(
628 context,
629 "Not moving the message {} that we have seen before.", &message_id
630 );
631 folder
632 } else {
633 _target = target_folder(context, folder, folder_meaning, &headers).await?;
634 &_target
635 }
636 } else {
637 warn!(
641 context,
642 "Not moving the message that does not have a Message-ID."
643 );
644 folder
645 };
646
647 let message_id = message_id.unwrap_or_else(create_message_id);
650
651 context
652 .sql
653 .execute(
654 "INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
655 VALUES (?1, ?2, ?3, ?4, ?5)
656 ON CONFLICT(folder, uid, uidvalidity)
657 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
658 target=excluded.target",
659 (&message_id, &folder, uid, uid_validity, target),
660 )
661 .await?;
662
663 if folder == target
670 && folder_meaning != FolderMeaning::Spam
675 && prefetch_should_download(
676 context,
677 &headers,
678 &message_id,
679 fetch_response.flags(),
680 )
681 .await.context("prefetch_should_download")?
682 {
683 match download_limit {
684 Some(download_limit) => uids_fetch.push((
685 uid,
686 fetch_response.size.unwrap_or_default() > download_limit,
687 )),
688 None => uids_fetch.push((uid, false)),
689 }
690 uid_message_ids.insert(uid, message_id);
691 } else {
692 largest_uid_skipped = Some(uid);
693 }
694 }
695
696 if !uids_fetch.is_empty() {
697 self.connectivity.set_working(context).await;
698 }
699
700 let mut largest_uid_fetched: u32 = 0;
702 let mut received_msgs = Vec::with_capacity(uids_fetch.len());
703 let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
704 let mut fetch_partially = false;
705 uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
706 for (uid, fp) in uids_fetch {
707 if fp != fetch_partially {
708 let (largest_uid_fetched_in_batch, received_msgs_in_batch) = session
709 .fetch_many_msgs(
710 context,
711 folder,
712 uid_validity,
713 uids_fetch_in_batch.split_off(0),
714 &uid_message_ids,
715 fetch_partially,
716 )
717 .await
718 .context("fetch_many_msgs")?;
719 received_msgs.extend(received_msgs_in_batch);
720 largest_uid_fetched = max(
721 largest_uid_fetched,
722 largest_uid_fetched_in_batch.unwrap_or(0),
723 );
724 fetch_partially = fp;
725 }
726 uids_fetch_in_batch.push(uid);
727 }
728
729 let mailbox_uid_next = session
735 .selected_mailbox
736 .as_ref()
737 .with_context(|| format!("Expected {folder:?} to be selected"))?
738 .uid_next
739 .unwrap_or_default();
740 let new_uid_next = max(
741 max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0)) + 1,
742 mailbox_uid_next,
743 );
744
745 if new_uid_next > old_uid_next {
746 set_uid_next(context, folder, new_uid_next).await?;
747 }
748
749 info!(context, "{} mails read from \"{}\".", read_cnt, folder);
750
751 if !received_msgs.is_empty() {
752 context.emit_event(EventType::IncomingMsgBunch);
753 }
754
755 chat::mark_old_messages_as_noticed(context, received_msgs).await?;
756
757 Ok(read_cnt > 0)
758 }
759
760 pub(crate) async fn fetch_existing_msgs(
766 &mut self,
767 context: &Context,
768 session: &mut Session,
769 ) -> Result<()> {
770 add_all_recipients_as_contacts(context, session, Config::ConfiguredSentboxFolder)
771 .await
772 .context("failed to get recipients from the sentbox")?;
773 add_all_recipients_as_contacts(context, session, Config::ConfiguredMvboxFolder)
774 .await
775 .context("failed to get recipients from the movebox")?;
776 add_all_recipients_as_contacts(context, session, Config::ConfiguredInboxFolder)
777 .await
778 .context("failed to get recipients from the inbox")?;
779
780 info!(context, "Done fetching existing messages.");
781 Ok(())
782 }
783}
784
785impl Session {
786 pub(crate) async fn resync_folders(&mut self, context: &Context) -> Result<()> {
788 let all_folders = self
789 .list_folders()
790 .await
791 .context("listing folders for resync")?;
792 for folder in all_folders {
793 let folder_meaning = get_folder_meaning(&folder);
794 if folder_meaning != FolderMeaning::Virtual {
795 self.resync_folder_uids(context, folder.name(), folder_meaning)
796 .await?;
797 }
798 }
799 Ok(())
800 }
801
802 pub(crate) async fn resync_folder_uids(
809 &mut self,
810 context: &Context,
811 folder: &str,
812 folder_meaning: FolderMeaning,
813 ) -> Result<()> {
814 let uid_validity;
815 let mut msgs = BTreeMap::new();
817
818 let create = false;
819 let folder_exists = self
820 .select_with_uidvalidity(context, folder, create)
821 .await?;
822 if folder_exists {
823 let mut list = self
824 .uid_fetch("1:*", RFC724MID_UID)
825 .await
826 .with_context(|| format!("Can't resync folder {folder}"))?;
827 while let Some(fetch) = list.try_next().await? {
828 let headers = match get_fetch_headers(&fetch) {
829 Ok(headers) => headers,
830 Err(err) => {
831 warn!(context, "Failed to parse FETCH headers: {}", err);
832 continue;
833 }
834 };
835 let message_id = prefetch_get_message_id(&headers);
836
837 if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
838 msgs.insert(
839 uid,
840 (
841 rfc724_mid,
842 target_folder(context, folder, folder_meaning, &headers).await?,
843 ),
844 );
845 }
846 }
847
848 info!(
849 context,
850 "resync_folder_uids: Collected {} message IDs in {folder}.",
851 msgs.len(),
852 );
853
854 uid_validity = get_uidvalidity(context, folder).await?;
855 } else {
856 warn!(context, "resync_folder_uids: No folder {folder}.");
857 uid_validity = 0;
858 }
859
860 context
862 .sql
863 .transaction(move |transaction| {
864 transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
865 for (uid, (rfc724_mid, target)) in &msgs {
866 transaction.execute(
869 "INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
870 VALUES (?1, ?2, ?3, ?4, ?5)
871 ON CONFLICT(folder, uid, uidvalidity)
872 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
873 target=excluded.target",
874 (rfc724_mid, folder, uid, uid_validity, target),
875 )?;
876 }
877 Ok(())
878 })
879 .await?;
880 Ok(())
881 }
882
883 async fn delete_message_batch(
886 &mut self,
887 context: &Context,
888 uid_set: &str,
889 row_ids: Vec<i64>,
890 ) -> Result<()> {
891 self.add_flag_finalized_with_set(uid_set, "\\Deleted")
893 .await?;
894 context
895 .sql
896 .transaction(|transaction| {
897 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
898 for row_id in row_ids {
899 stmt.execute((row_id,))?;
900 }
901 Ok(())
902 })
903 .await
904 .context("Cannot remove deleted messages from imap table")?;
905
906 context.emit_event(EventType::ImapMessageDeleted(format!(
907 "IMAP messages {uid_set} marked as deleted"
908 )));
909 Ok(())
910 }
911
912 async fn move_message_batch(
915 &mut self,
916 context: &Context,
917 set: &str,
918 row_ids: Vec<i64>,
919 target: &str,
920 ) -> Result<()> {
921 if self.can_move() {
922 match self.uid_mv(set, &target).await {
923 Ok(()) => {
924 context
926 .sql
927 .transaction(|transaction| {
928 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
929 for row_id in row_ids {
930 stmt.execute((row_id,))?;
931 }
932 Ok(())
933 })
934 .await
935 .context("Cannot delete moved messages from imap table")?;
936 context.emit_event(EventType::ImapMessageMoved(format!(
937 "IMAP messages {set} moved to {target}"
938 )));
939 return Ok(());
940 }
941 Err(err) => {
942 if context.should_delete_to_trash().await? {
943 error!(
944 context,
945 "Cannot move messages {} to {}, no fallback to COPY/DELETE because \
946 delete_to_trash is set. Error: {:#}",
947 set,
948 target,
949 err,
950 );
951 return Err(err.into());
952 }
953 warn!(
954 context,
955 "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
956 set,
957 target,
958 err
959 );
960 }
961 }
962 }
963
964 let copy = !context.is_trash(target).await?;
967 if copy {
968 info!(
969 context,
970 "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
971 );
972 self.uid_copy(&set, &target).await?;
973 } else {
974 error!(
975 context,
976 "Server does not support MOVE, fallback to DELETE {} to {}", set, target,
977 );
978 }
979 context
980 .sql
981 .transaction(|transaction| {
982 let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
983 for row_id in row_ids {
984 stmt.execute((row_id,))?;
985 }
986 Ok(())
987 })
988 .await
989 .context("Cannot plan deletion of messages")?;
990 if copy {
991 context.emit_event(EventType::ImapMessageMoved(format!(
992 "IMAP messages {set} copied to {target}"
993 )));
994 }
995 Ok(())
996 }
997
998 async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
1002 let rows = context
1003 .sql
1004 .query_map(
1005 "SELECT id, uid, target FROM imap
1006 WHERE folder = ?
1007 AND target != folder
1008 ORDER BY target, uid",
1009 (folder,),
1010 |row| {
1011 let rowid: i64 = row.get(0)?;
1012 let uid: u32 = row.get(1)?;
1013 let target: String = row.get(2)?;
1014 Ok((rowid, uid, target))
1015 },
1016 |rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
1017 )
1018 .await?;
1019
1020 for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
1021 let create = false;
1026 let folder_exists = self
1027 .select_with_uidvalidity(context, folder, create)
1028 .await?;
1029 ensure!(folder_exists, "No folder {folder}");
1030
1031 if target.is_empty() {
1033 self.delete_message_batch(context, &uid_set, rowid_set)
1034 .await
1035 .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
1036 } else {
1037 self.move_message_batch(context, &uid_set, rowid_set, &target)
1038 .await
1039 .with_context(|| {
1040 format!(
1041 "cannot move batch of messages {:?} to folder {:?}",
1042 &uid_set, target
1043 )
1044 })?;
1045 }
1046 }
1047
1048 if let Err(err) = self.maybe_close_folder(context).await {
1051 warn!(context, "failed to close folder: {:?}", err);
1052 }
1053
1054 Ok(())
1055 }
1056
1057 pub(crate) async fn send_sync_msgs(&mut self, context: &Context, folder: &str) -> Result<()> {
1059 context.send_sync_msg().await?;
1060 while let Some((id, mime, msg_id, attempts)) = context
1061 .sql
1062 .query_row_optional(
1063 "SELECT id, mime, msg_id, attempts FROM imap_send ORDER BY id LIMIT 1",
1064 (),
1065 |row| {
1066 let id: i64 = row.get(0)?;
1067 let mime: String = row.get(1)?;
1068 let msg_id: MsgId = row.get(2)?;
1069 let attempts: i64 = row.get(3)?;
1070 Ok((id, mime, msg_id, attempts))
1071 },
1072 )
1073 .await
1074 .context("Failed to SELECT from imap_send")?
1075 {
1076 let res = self
1077 .append(folder, Some("(\\Seen)"), None, mime)
1078 .await
1079 .with_context(|| format!("IMAP APPEND to {folder} failed for {msg_id}"))
1080 .log_err(context);
1081 if res.is_ok() {
1082 msg_id.set_delivered(context).await?;
1083 }
1084 const MAX_ATTEMPTS: i64 = 2;
1085 if res.is_ok() || attempts >= MAX_ATTEMPTS - 1 {
1086 context
1087 .sql
1088 .execute("DELETE FROM imap_send WHERE id=?", (id,))
1089 .await
1090 .context("Failed to delete from imap_send")?;
1091 } else {
1092 context
1093 .sql
1094 .execute("UPDATE imap_send SET attempts=attempts+1 WHERE id=?", (id,))
1095 .await
1096 .context("Failed to update imap_send.attempts")?;
1097 res?;
1098 }
1099 }
1100 Ok(())
1101 }
1102
1103 pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
1105 let rows = context
1106 .sql
1107 .query_map(
1108 "SELECT imap.id, uid, folder FROM imap, imap_markseen
1109 WHERE imap.id = imap_markseen.id AND target = folder
1110 ORDER BY folder, uid",
1111 [],
1112 |row| {
1113 let rowid: i64 = row.get(0)?;
1114 let uid: u32 = row.get(1)?;
1115 let folder: String = row.get(2)?;
1116 Ok((rowid, uid, folder))
1117 },
1118 |rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
1119 )
1120 .await?;
1121
1122 for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1123 let create = false;
1124 let folder_exists = match self.select_with_uidvalidity(context, &folder, create).await {
1125 Err(err) => {
1126 warn!(
1127 context,
1128 "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}.");
1129 continue;
1130 }
1131 Ok(folder_exists) => folder_exists,
1132 };
1133 if !folder_exists {
1134 warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1135 } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1136 warn!(
1137 context,
1138 "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}.");
1139 continue;
1140 } else {
1141 info!(
1142 context,
1143 "Marked messages {} in folder {} as seen.", uid_set, folder
1144 );
1145 }
1146 context
1147 .sql
1148 .transaction(|transaction| {
1149 let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1150 for rowid in rowid_set {
1151 stmt.execute((rowid,))?;
1152 }
1153 Ok(())
1154 })
1155 .await
1156 .context("Cannot remove messages marked as seen from imap_markseen table")?;
1157 }
1158
1159 Ok(())
1160 }
1161
1162 pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1164 if !self.can_condstore() {
1165 info!(
1166 context,
1167 "Server does not support CONDSTORE, skipping flag synchronization."
1168 );
1169 return Ok(());
1170 }
1171
1172 let create = false;
1173 let folder_exists = self
1174 .select_with_uidvalidity(context, folder, create)
1175 .await
1176 .context("Failed to select folder")?;
1177 if !folder_exists {
1178 return Ok(());
1179 }
1180
1181 let mailbox = self
1182 .selected_mailbox
1183 .as_ref()
1184 .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1185
1186 if mailbox.highest_modseq.is_none() {
1189 info!(
1190 context,
1191 "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1192 );
1193 return Ok(());
1194 }
1195
1196 let mut updated_chat_ids = BTreeSet::new();
1197 let uid_validity = get_uidvalidity(context, folder)
1198 .await
1199 .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1200 let mut highest_modseq = get_modseq(context, folder)
1201 .await
1202 .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1203 let mut list = self
1204 .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1205 .await
1206 .context("failed to fetch flags")?;
1207
1208 let mut got_unsolicited_fetch = false;
1209
1210 while let Some(fetch) = list
1211 .try_next()
1212 .await
1213 .context("failed to get FETCH result")?
1214 {
1215 let uid = if let Some(uid) = fetch.uid {
1216 uid
1217 } else {
1218 info!(context, "FETCH result contains no UID, skipping");
1219 got_unsolicited_fetch = true;
1220 continue;
1221 };
1222 let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1223 if is_seen {
1224 if let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
1225 .await
1226 .with_context(|| {
1227 format!("failed to update seen status for msg {folder}/{uid}")
1228 })?
1229 {
1230 updated_chat_ids.insert(chat_id);
1231 }
1232 }
1233
1234 if let Some(modseq) = fetch.modseq {
1235 if modseq > highest_modseq {
1236 highest_modseq = modseq;
1237 }
1238 } else {
1239 warn!(context, "FETCH result contains no MODSEQ");
1240 }
1241 }
1242 drop(list);
1243
1244 if got_unsolicited_fetch {
1245 self.new_mail = true;
1250 }
1251
1252 set_modseq(context, folder, highest_modseq)
1253 .await
1254 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1255 if !updated_chat_ids.is_empty() {
1256 context.on_archived_chats_maybe_noticed();
1257 }
1258 for updated_chat_id in updated_chat_ids {
1259 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1260 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1261 }
1262
1263 Ok(())
1264 }
1265
1266 pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
1268 let mut uids: Vec<_> = self
1269 .uid_search(get_imap_self_sent_search_command(context).await?)
1270 .await?
1271 .into_iter()
1272 .collect();
1273 uids.sort_unstable();
1274
1275 let mut result = Vec::new();
1276 for (_, uid_set) in build_sequence_sets(&uids)? {
1277 let mut list = self
1278 .uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
1279 .await
1280 .context("IMAP Could not fetch")?;
1281
1282 while let Some(msg) = list.try_next().await? {
1283 match get_fetch_headers(&msg) {
1284 Ok(headers) => {
1285 if let Some(from) = mimeparser::get_from(&headers) {
1286 if context.is_self_addr(&from.addr).await? {
1287 result.extend(mimeparser::get_recipients(&headers));
1288 }
1289 }
1290 }
1291 Err(err) => {
1292 warn!(context, "{}", err);
1293 continue;
1294 }
1295 };
1296 }
1297 }
1298 Ok(result)
1299 }
1300
1301 pub(crate) async fn fetch_many_msgs(
1307 &mut self,
1308 context: &Context,
1309 folder: &str,
1310 uidvalidity: u32,
1311 request_uids: Vec<u32>,
1312 uid_message_ids: &BTreeMap<u32, String>,
1313 fetch_partially: bool,
1314 ) -> Result<(Option<u32>, Vec<ReceivedMsg>)> {
1315 let mut last_uid = None;
1316 let mut received_msgs = Vec::new();
1317
1318 if request_uids.is_empty() {
1319 return Ok((last_uid, received_msgs));
1320 }
1321
1322 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1323 info!(
1324 context,
1325 "Starting a {} FETCH of message set \"{}\".",
1326 if fetch_partially { "partial" } else { "full" },
1327 set
1328 );
1329 let mut fetch_responses = self
1330 .uid_fetch(
1331 &set,
1332 if fetch_partially {
1333 BODY_PARTIAL
1334 } else {
1335 BODY_FULL
1336 },
1337 )
1338 .await
1339 .with_context(|| {
1340 format!("fetching messages {} from folder \"{}\"", &set, folder)
1341 })?;
1342
1343 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1346
1347 let mut count = 0;
1348 for &request_uid in &request_uids {
1349 let mut fetch_response = uid_msgs.remove(&request_uid);
1351
1352 while fetch_response.is_none() {
1354 let next_fetch_response =
1355 if let Some(next_fetch_response) = fetch_responses.next().await {
1356 next_fetch_response
1357 } else {
1358 break;
1360 };
1361
1362 let next_fetch_response =
1363 next_fetch_response.context("Failed to process IMAP FETCH result")?;
1364
1365 if let Some(next_uid) = next_fetch_response.uid {
1366 if next_uid == request_uid {
1367 fetch_response = Some(next_fetch_response);
1368 } else if !request_uids.contains(&next_uid) {
1369 info!(
1376 context,
1377 "Skipping not requested FETCH response for UID {}.", next_uid
1378 );
1379 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1380 warn!(context, "Got duplicated UID {}.", next_uid);
1381 }
1382 } else {
1383 info!(context, "Skipping FETCH response without UID.");
1384 }
1385 }
1386
1387 let fetch_response = match fetch_response {
1388 Some(fetch) => fetch,
1389 None => {
1390 warn!(
1391 context,
1392 "Missed UID {} in the server response.", request_uid
1393 );
1394 continue;
1395 }
1396 };
1397 count += 1;
1398
1399 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1400 let (body, partial) = if fetch_partially {
1401 (fetch_response.header(), fetch_response.size) } else {
1403 (fetch_response.body(), None) };
1405
1406 if is_deleted {
1407 info!(context, "Not processing deleted msg {}.", request_uid);
1408 last_uid = Some(request_uid);
1409 continue;
1410 }
1411
1412 let body = if let Some(body) = body {
1413 body
1414 } else {
1415 info!(
1416 context,
1417 "Not processing message {} without a BODY.", request_uid
1418 );
1419 last_uid = Some(request_uid);
1420 continue;
1421 };
1422
1423 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1424
1425 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1426 error!(
1427 context,
1428 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1429 request_uid
1430 );
1431 continue;
1432 };
1433
1434 info!(
1435 context,
1436 "Passing message UID {} to receive_imf().", request_uid
1437 );
1438 match receive_imf_inner(
1439 context,
1440 folder,
1441 uidvalidity,
1442 request_uid,
1443 rfc724_mid,
1444 body,
1445 is_seen,
1446 partial,
1447 )
1448 .await
1449 {
1450 Ok(received_msg) => {
1451 if let Some(m) = received_msg {
1452 received_msgs.push(m);
1453 }
1454 }
1455 Err(err) => {
1456 warn!(context, "receive_imf error: {:#}.", err);
1457 }
1458 };
1459 last_uid = Some(request_uid)
1460 }
1461
1462 while fetch_responses.next().await.is_some() {}
1465
1466 if count != request_uids.len() {
1467 warn!(
1468 context,
1469 "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1470 count,
1471 request_uids.len(),
1472 request_uids,
1473 );
1474 } else {
1475 info!(
1476 context,
1477 "Successfully received {} UIDs.",
1478 request_uids.len()
1479 );
1480 }
1481 }
1482
1483 Ok((last_uid, received_msgs))
1484 }
1485
1486 pub(crate) async fn fetch_metadata(&mut self, context: &Context) -> Result<()> {
1492 if !self.can_metadata() {
1493 return Ok(());
1494 }
1495
1496 let mut lock = context.metadata.write().await;
1497 if (*lock).is_some() {
1498 return Ok(());
1499 }
1500
1501 info!(
1502 context,
1503 "Server supports metadata, retrieving server comment and admin contact."
1504 );
1505
1506 let mut comment = None;
1507 let mut admin = None;
1508 let mut iroh_relay = None;
1509
1510 let mailbox = "";
1511 let options = "";
1512 let metadata = self
1513 .get_metadata(
1514 mailbox,
1515 options,
1516 "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay)",
1517 )
1518 .await?;
1519 for m in metadata {
1520 match m.entry.as_ref() {
1521 "/shared/comment" => {
1522 comment = m.value;
1523 }
1524 "/shared/admin" => {
1525 admin = m.value;
1526 }
1527 "/shared/vendor/deltachat/irohrelay" => {
1528 if let Some(value) = m.value {
1529 if let Ok(url) = Url::parse(&value) {
1530 iroh_relay = Some(url);
1531 } else {
1532 warn!(
1533 context,
1534 "Got invalid URL from iroh relay metadata: {:?}.", value
1535 );
1536 }
1537 }
1538 }
1539 _ => {}
1540 }
1541 }
1542 *lock = Some(ServerMetadata {
1543 comment,
1544 admin,
1545 iroh_relay,
1546 });
1547 Ok(())
1548 }
1549
1550 pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1552 if context.push_subscribed.load(Ordering::Relaxed) {
1553 return Ok(());
1554 }
1555
1556 let Some(device_token) = context.push_subscriber.device_token().await else {
1557 return Ok(());
1558 };
1559
1560 if self.can_metadata() && self.can_push() {
1561 let old_encrypted_device_token =
1562 context.get_config(Config::EncryptedDeviceToken).await?;
1563
1564 let device_token_changed = old_encrypted_device_token.is_none()
1566 || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1567
1568 let new_encrypted_device_token;
1569 if device_token_changed {
1570 let encrypted_device_token = encrypt_device_token(&device_token)
1571 .context("Failed to encrypt device token")?;
1572
1573 let encrypted_device_token_len = encrypted_device_token.len();
1577
1578 context
1584 .set_config_internal(Config::DeviceToken, Some(&device_token))
1585 .await?;
1586 context
1587 .set_config_internal(
1588 Config::EncryptedDeviceToken,
1589 Some(&encrypted_device_token),
1590 )
1591 .await?;
1592
1593 if encrypted_device_token_len <= 4096 {
1594 new_encrypted_device_token = Some(encrypted_device_token);
1595 } else {
1596 warn!(context, "Device token is too long for LITERAL-, ignoring.");
1606 new_encrypted_device_token = None;
1607 }
1608 } else {
1609 new_encrypted_device_token = old_encrypted_device_token;
1610 }
1611
1612 if let Some(encrypted_device_token) = new_encrypted_device_token {
1615 let folder = context
1616 .get_config(Config::ConfiguredInboxFolder)
1617 .await?
1618 .context("INBOX is not configured")?;
1619
1620 self.run_command_and_check_ok(&format_setmetadata(
1621 &folder,
1622 &encrypted_device_token,
1623 ))
1624 .await
1625 .context("SETMETADATA command failed")?;
1626
1627 context.push_subscribed.store(true, Ordering::Relaxed);
1628 }
1629 } else if !context.push_subscriber.heartbeat_subscribed().await {
1630 let context = context.clone();
1631 tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1633 }
1634
1635 Ok(())
1636 }
1637}
1638
1639fn format_setmetadata(folder: &str, device_token: &str) -> String {
1640 let device_token_len = device_token.len();
1641 format!(
1642 "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1643 )
1644}
1645
1646impl Session {
1647 async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1653 if flag == "\\Deleted" {
1654 self.selected_folder_needs_expunge = true;
1655 }
1656 let query = format!("+FLAGS ({flag})");
1657 let mut responses = self
1658 .uid_store(uid_set, &query)
1659 .await
1660 .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1661 while let Some(_response) = responses.next().await {
1662 }
1664 Ok(())
1665 }
1666
1667 async fn configure_mvbox<'a>(
1676 &mut self,
1677 context: &Context,
1678 folders: &[&'a str],
1679 create_mvbox: bool,
1680 ) -> Result<Option<&'a str>> {
1681 self.maybe_close_folder(context).await?;
1684
1685 for folder in folders {
1686 info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1687 let res = self.examine(&folder).await;
1688 if res.is_ok() {
1689 info!(
1690 context,
1691 "MVBOX-folder {:?} successfully selected, using it.", &folder
1692 );
1693 self.close().await?;
1694 let create = false;
1697 let folder_exists = self
1698 .select_with_uidvalidity(context, folder, create)
1699 .await?;
1700 ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1701 return Ok(Some(folder));
1702 }
1703 }
1704
1705 if !create_mvbox {
1706 return Ok(None);
1707 }
1708 for folder in folders {
1711 match self
1712 .select_with_uidvalidity(context, folder, create_mvbox)
1713 .await
1714 {
1715 Ok(_) => {
1716 info!(context, "MVBOX-folder {} created.", folder);
1717 return Ok(Some(folder));
1718 }
1719 Err(err) => {
1720 warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
1721 }
1722 }
1723 }
1724 Ok(None)
1725 }
1726}
1727
1728impl Imap {
1729 pub(crate) async fn configure_folders(
1730 &mut self,
1731 context: &Context,
1732 session: &mut Session,
1733 create_mvbox: bool,
1734 ) -> Result<()> {
1735 let mut folders = session
1736 .list(Some(""), Some("*"))
1737 .await
1738 .context("list_folders failed")?;
1739 let mut delimiter = ".".to_string();
1740 let mut delimiter_is_default = true;
1741 let mut folder_configs = BTreeMap::new();
1742
1743 while let Some(folder) = folders.try_next().await? {
1744 info!(context, "Scanning folder: {:?}", folder);
1745
1746 if let Some(d) = folder.delimiter() {
1748 if delimiter_is_default && !d.is_empty() && delimiter != d {
1749 delimiter = d.to_string();
1750 delimiter_is_default = false;
1751 }
1752 }
1753
1754 let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1755 let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1756 if let Some(config) = folder_meaning.to_config() {
1757 folder_configs.insert(config, folder.name().to_string());
1759 } else if let Some(config) = folder_name_meaning.to_config() {
1760 folder_configs
1762 .entry(config)
1763 .or_insert_with(|| folder.name().to_string());
1764 }
1765 }
1766 drop(folders);
1767
1768 info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1769
1770 let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1771 let mvbox_folder = session
1772 .configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
1773 .await
1774 .context("failed to configure mvbox")?;
1775
1776 context
1777 .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1778 .await?;
1779 if let Some(mvbox_folder) = mvbox_folder {
1780 info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1781 context
1782 .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1783 .await?;
1784 }
1785 for (config, name) in folder_configs {
1786 context.set_config_internal(config, Some(&name)).await?;
1787 }
1788 context
1789 .sql
1790 .set_raw_config_int(
1791 constants::DC_FOLDERS_CONFIGURED_KEY,
1792 constants::DC_FOLDERS_CONFIGURED_VERSION,
1793 )
1794 .await?;
1795
1796 info!(context, "FINISHED configuring IMAP-folders.");
1797 Ok(())
1798 }
1799}
1800
1801impl Session {
1802 fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1811 use async_imap::imap_proto::Response;
1812 use async_imap::imap_proto::ResponseCode;
1813 use UnsolicitedResponse::*;
1814
1815 let folder = self.selected_folder.as_deref().unwrap_or_default();
1816 let mut should_refetch = false;
1817 while let Ok(response) = self.unsolicited_responses.try_recv() {
1818 match response {
1819 Exists(_) => {
1820 info!(
1821 context,
1822 "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1823 );
1824 should_refetch = true;
1825 }
1826
1827 Expunge(_) | Recent(_) => {}
1828 Other(ref response_data) => {
1829 match response_data.parsed() {
1830 Response::Fetch { .. } => {
1831 info!(
1832 context,
1833 "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1834 );
1835 should_refetch = true;
1836 }
1837
1838 Response::Done {
1841 code: Some(ResponseCode::CopyUid(_, _, _)),
1842 ..
1843 } => {}
1844
1845 _ => {
1846 info!(context, "{folder:?}: got unsolicited response {response:?}")
1847 }
1848 }
1849 }
1850 _ => {
1851 info!(context, "{folder:?}: got unsolicited response {response:?}")
1852 }
1853 }
1854 }
1855 Ok(should_refetch)
1856 }
1857}
1858
1859async fn should_move_out_of_spam(
1860 context: &Context,
1861 headers: &[mailparse::MailHeader<'_>],
1862) -> Result<bool> {
1863 if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1864 return Ok(true);
1875 }
1876
1877 if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1878 if msg.chat_blocked != Blocked::Not {
1879 return Ok(false);
1881 }
1882 } else {
1883 let from = match mimeparser::get_from(headers) {
1884 Some(f) => f,
1885 None => return Ok(false),
1886 };
1887 let (from_id, blocked_contact, _origin) =
1889 match from_field_to_contact_id(context, &from, true)
1890 .await
1891 .context("from_field_to_contact_id")?
1892 {
1893 Some(res) => res,
1894 None => {
1895 warn!(
1896 context,
1897 "Contact with From address {:?} cannot exist, not moving out of spam", from
1898 );
1899 return Ok(false);
1900 }
1901 };
1902 if blocked_contact {
1903 return Ok(false);
1905 }
1906
1907 if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
1908 if chat_id_blocked.blocked != Blocked::Not {
1909 return Ok(false);
1910 }
1911 } else if from_id != ContactId::SELF {
1912 return Ok(false);
1914 }
1915 }
1916
1917 Ok(true)
1918}
1919
1920async fn spam_target_folder_cfg(
1925 context: &Context,
1926 headers: &[mailparse::MailHeader<'_>],
1927) -> Result<Option<Config>> {
1928 if !should_move_out_of_spam(context, headers).await? {
1929 return Ok(None);
1930 }
1931
1932 if needs_move_to_mvbox(context, headers).await?
1933 || context.get_config_bool(Config::OnlyFetchMvbox).await?
1936 {
1937 Ok(Some(Config::ConfiguredMvboxFolder))
1938 } else {
1939 Ok(Some(Config::ConfiguredInboxFolder))
1940 }
1941}
1942
1943pub async fn target_folder_cfg(
1946 context: &Context,
1947 folder: &str,
1948 folder_meaning: FolderMeaning,
1949 headers: &[mailparse::MailHeader<'_>],
1950) -> Result<Option<Config>> {
1951 if context.is_mvbox(folder).await? {
1952 return Ok(None);
1953 }
1954
1955 if folder_meaning == FolderMeaning::Spam {
1956 spam_target_folder_cfg(context, headers).await
1957 } else if needs_move_to_mvbox(context, headers).await? {
1958 Ok(Some(Config::ConfiguredMvboxFolder))
1959 } else {
1960 Ok(None)
1961 }
1962}
1963
1964pub async fn target_folder(
1965 context: &Context,
1966 folder: &str,
1967 folder_meaning: FolderMeaning,
1968 headers: &[mailparse::MailHeader<'_>],
1969) -> Result<String> {
1970 match target_folder_cfg(context, folder, folder_meaning, headers).await? {
1971 Some(config) => match context.get_config(config).await? {
1972 Some(target) => Ok(target),
1973 None => Ok(folder.to_string()),
1974 },
1975 None => Ok(folder.to_string()),
1976 }
1977}
1978
1979async fn needs_move_to_mvbox(
1980 context: &Context,
1981 headers: &[mailparse::MailHeader<'_>],
1982) -> Result<bool> {
1983 let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
1984 if !context.get_config_bool(Config::IsChatmail).await?
1985 && has_chat_version
1986 && headers
1987 .get_header_value(HeaderDef::AutoSubmitted)
1988 .filter(|val| val.eq_ignore_ascii_case("auto-generated"))
1989 .is_some()
1990 {
1991 if let Some(from) = mimeparser::get_from(headers) {
1992 if context.is_self_addr(&from.addr).await? {
1993 return Ok(true);
1994 }
1995 }
1996 }
1997 if !context.get_config_bool(Config::MvboxMove).await? {
1998 return Ok(false);
1999 }
2000
2001 if headers
2002 .get_header_value(HeaderDef::AutocryptSetupMessage)
2003 .is_some()
2004 {
2005 return Ok(false);
2008 }
2009
2010 if has_chat_version {
2011 Ok(true)
2012 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
2013 match parent.is_dc_message {
2014 MessengerMessage::No => Ok(false),
2015 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
2016 }
2017 } else {
2018 Ok(false)
2019 }
2020}
2021
2022fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2029 const SENT_NAMES: &[&str] = &[
2031 "sent",
2032 "sentmail",
2033 "sent objects",
2034 "gesendet",
2035 "Sent Mail",
2036 "Sendte e-mails",
2037 "Enviados",
2038 "Messages envoyés",
2039 "Messages envoyes",
2040 "Posta inviata",
2041 "Verzonden berichten",
2042 "Wyslane",
2043 "E-mails enviados",
2044 "Correio enviado",
2045 "Enviada",
2046 "Enviado",
2047 "Gönderildi",
2048 "Inviati",
2049 "Odeslaná pošta",
2050 "Sendt",
2051 "Skickat",
2052 "Verzonden",
2053 "Wysłane",
2054 "Éléments envoyés",
2055 "Απεσταλμένα",
2056 "Отправленные",
2057 "寄件備份",
2058 "已发送邮件",
2059 "送信済み",
2060 "보낸편지함",
2061 ];
2062 const SPAM_NAMES: &[&str] = &[
2063 "spam",
2064 "junk",
2065 "Correio electrónico não solicitado",
2066 "Correo basura",
2067 "Lixo",
2068 "Nettsøppel",
2069 "Nevyžádaná pošta",
2070 "No solicitado",
2071 "Ongewenst",
2072 "Posta indesiderata",
2073 "Skräp",
2074 "Wiadomości-śmieci",
2075 "Önemsiz",
2076 "Ανεπιθύμητα",
2077 "Спам",
2078 "垃圾邮件",
2079 "垃圾郵件",
2080 "迷惑メール",
2081 "스팸",
2082 ];
2083 const DRAFT_NAMES: &[&str] = &[
2084 "Drafts",
2085 "Kladder",
2086 "Entw?rfe",
2087 "Borradores",
2088 "Brouillons",
2089 "Bozze",
2090 "Concepten",
2091 "Wersje robocze",
2092 "Rascunhos",
2093 "Entwürfe",
2094 "Koncepty",
2095 "Kopie robocze",
2096 "Taslaklar",
2097 "Utkast",
2098 "Πρόχειρα",
2099 "Черновики",
2100 "下書き",
2101 "草稿",
2102 "임시보관함",
2103 ];
2104 const TRASH_NAMES: &[&str] = &[
2105 "Trash",
2106 "Bin",
2107 "Caixote do lixo",
2108 "Cestino",
2109 "Corbeille",
2110 "Papelera",
2111 "Papierkorb",
2112 "Papirkurv",
2113 "Papperskorgen",
2114 "Prullenbak",
2115 "Rubujo",
2116 "Κάδος απορριμμάτων",
2117 "Корзина",
2118 "Кошик",
2119 "ゴミ箱",
2120 "垃圾桶",
2121 "已删除邮件",
2122 "휴지통",
2123 ];
2124 let lower = folder_name.to_lowercase();
2125
2126 if SENT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2127 FolderMeaning::Sent
2128 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2129 FolderMeaning::Spam
2130 } else if DRAFT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2131 FolderMeaning::Drafts
2132 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2133 FolderMeaning::Trash
2134 } else {
2135 FolderMeaning::Unknown
2136 }
2137}
2138
2139fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2140 for attr in folder_attrs {
2141 match attr {
2142 NameAttribute::Trash => return FolderMeaning::Trash,
2143 NameAttribute::Sent => return FolderMeaning::Sent,
2144 NameAttribute::Junk => return FolderMeaning::Spam,
2145 NameAttribute::Drafts => return FolderMeaning::Drafts,
2146 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2147 NameAttribute::Extension(ref label) => {
2148 match label.as_ref() {
2149 "\\Spam" => return FolderMeaning::Spam,
2150 "\\Important" => return FolderMeaning::Virtual,
2151 _ => {}
2152 };
2153 }
2154 _ => {}
2155 }
2156 }
2157 FolderMeaning::Unknown
2158}
2159
2160pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2161 match get_folder_meaning_by_attrs(folder.attributes()) {
2162 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2163 meaning => meaning,
2164 }
2165}
2166
2167fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
2169 match prefetch_msg.header() {
2170 Some(header_bytes) => {
2171 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2172 Ok(headers)
2173 }
2174 None => Ok(Vec::new()),
2175 }
2176}
2177
2178pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2179 headers
2180 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2181 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2182 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2183}
2184
2185pub(crate) fn create_message_id() -> String {
2186 format!("{}{}", GENERATED_PREFIX, create_id())
2187}
2188
2189async fn prefetch_get_chat(
2191 context: &Context,
2192 headers: &[mailparse::MailHeader<'_>],
2193) -> Result<Option<chat::Chat>> {
2194 let parent = get_prefetch_parent_message(context, headers).await?;
2195 if let Some(parent) = &parent {
2196 return Ok(Some(
2197 chat::Chat::load_from_db(context, parent.get_chat_id()).await?,
2198 ));
2199 }
2200
2201 Ok(None)
2202}
2203
2204pub(crate) async fn prefetch_should_download(
2206 context: &Context,
2207 headers: &[mailparse::MailHeader<'_>],
2208 message_id: &str,
2209 mut flags: impl Iterator<Item = Flag<'_>>,
2210) -> Result<bool> {
2211 if message::rfc724_mid_exists(context, message_id)
2212 .await?
2213 .is_some()
2214 {
2215 markseen_on_imap_table(context, message_id).await?;
2216 return Ok(false);
2217 }
2218
2219 if let Some(chat) = prefetch_get_chat(context, headers).await? {
2223 if chat.typ == Chattype::Group && !chat.id.is_special() {
2224 return Ok(true);
2227 }
2228 }
2229
2230 let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2231 let from = from.to_ascii_lowercase();
2232 from.contains("mailer-daemon") || from.contains("mail-daemon")
2233 } else {
2234 false
2235 };
2236
2237 let is_autocrypt_setup_message = headers
2239 .get_header_value(HeaderDef::AutocryptSetupMessage)
2240 .is_some();
2241
2242 let from = match mimeparser::get_from(headers) {
2243 Some(f) => f,
2244 None => return Ok(false),
2245 };
2246 let (_from_id, blocked_contact, origin) =
2247 match from_field_to_contact_id(context, &from, true).await? {
2248 Some(res) => res,
2249 None => return Ok(false),
2250 };
2251 if flags.any(|f| f == Flag::Draft) {
2255 info!(context, "Ignoring draft message");
2256 return Ok(false);
2257 }
2258
2259 let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2260 let accepted_contact = origin.is_known();
2261 let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
2262 .await?
2263 .map(|parent| match parent.is_dc_message {
2264 MessengerMessage::No => false,
2265 MessengerMessage::Yes | MessengerMessage::Reply => true,
2266 })
2267 .unwrap_or_default();
2268
2269 let show_emails =
2270 ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2271
2272 let show = is_autocrypt_setup_message
2273 || match show_emails {
2274 ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2275 ShowEmails::AcceptedContacts => {
2276 is_chat_message || is_reply_to_chat_message || accepted_contact
2277 }
2278 ShowEmails::All => true,
2279 };
2280
2281 let should_download = (show && !blocked_contact) || maybe_ndn;
2282 Ok(should_download)
2283}
2284
2285pub(crate) fn is_dup_msg(is_chat_msg: bool, ts_sent: i64, ts_sent_old: i64) -> bool {
2287 is_chat_msg && ts_sent_old != 0 && ts_sent > ts_sent_old
2292}
2293
2294async fn mark_seen_by_uid(
2298 context: &Context,
2299 folder: &str,
2300 uid_validity: u32,
2301 uid: u32,
2302) -> Result<Option<ChatId>> {
2303 if let Some((msg_id, chat_id)) = context
2304 .sql
2305 .query_row_optional(
2306 "SELECT id, chat_id FROM msgs
2307 WHERE id > 9 AND rfc724_mid IN (
2308 SELECT rfc724_mid FROM imap
2309 WHERE folder=?1
2310 AND uidvalidity=?2
2311 AND uid=?3
2312 LIMIT 1
2313 )",
2314 (&folder, uid_validity, uid),
2315 |row| {
2316 let msg_id: MsgId = row.get(0)?;
2317 let chat_id: ChatId = row.get(1)?;
2318 Ok((msg_id, chat_id))
2319 },
2320 )
2321 .await
2322 .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2323 {
2324 let updated = context
2325 .sql
2326 .execute(
2327 "UPDATE msgs SET state=?1
2328 WHERE (state=?2 OR state=?3)
2329 AND id=?4",
2330 (
2331 MessageState::InSeen,
2332 MessageState::InFresh,
2333 MessageState::InNoticed,
2334 msg_id,
2335 ),
2336 )
2337 .await
2338 .with_context(|| format!("failed to update msg {msg_id} state"))?
2339 > 0;
2340
2341 if updated {
2342 msg_id
2343 .start_ephemeral_timer(context)
2344 .await
2345 .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2346 Ok(Some(chat_id))
2347 } else {
2348 Ok(None)
2350 }
2351 } else {
2352 Ok(None)
2354 }
2355}
2356
2357pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2360 context
2361 .sql
2362 .execute(
2363 "INSERT OR IGNORE INTO imap_markseen (id)
2364 SELECT id FROM imap WHERE rfc724_mid=?",
2365 (message_id,),
2366 )
2367 .await?;
2368 context.scheduler.interrupt_inbox().await;
2369
2370 Ok(())
2371}
2372
2373pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) -> Result<()> {
2377 context
2378 .sql
2379 .execute(
2380 "INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
2381 ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
2382 (folder, uid_next),
2383 )
2384 .await?;
2385 Ok(())
2386}
2387
2388async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
2394 Ok(context
2395 .sql
2396 .query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,))
2397 .await?
2398 .unwrap_or(0))
2399}
2400
2401pub(crate) async fn set_uidvalidity(
2402 context: &Context,
2403 folder: &str,
2404 uidvalidity: u32,
2405) -> Result<()> {
2406 context
2407 .sql
2408 .execute(
2409 "INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
2410 ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2411 (folder, uidvalidity),
2412 )
2413 .await?;
2414 Ok(())
2415}
2416
2417async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
2418 Ok(context
2419 .sql
2420 .query_get_value(
2421 "SELECT uidvalidity FROM imap_sync WHERE folder=?;",
2422 (folder,),
2423 )
2424 .await?
2425 .unwrap_or(0))
2426}
2427
2428pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) -> Result<()> {
2429 context
2430 .sql
2431 .execute(
2432 "INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
2433 ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
2434 (folder, modseq),
2435 )
2436 .await?;
2437 Ok(())
2438}
2439
2440async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
2441 Ok(context
2442 .sql
2443 .query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,))
2444 .await?
2445 .unwrap_or(0))
2446}
2447
2448pub(crate) async fn get_imap_self_sent_search_command(context: &Context) -> Result<String> {
2450 let mut search_command = format!("FROM \"{}\"", context.get_primary_self_addr().await?);
2452
2453 for item in context.get_secondary_self_addrs().await? {
2454 search_command = format!("OR ({search_command}) (FROM \"{item}\")");
2455 }
2456
2457 Ok(search_command)
2458}
2459
2460pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result<(u32, u32)> {
2462 let key = format!("imap.mailbox.{folder}");
2463 if let Some(entry) = context.sql.get_raw_config(&key).await? {
2464 let mut parts = entry.split(':');
2466 Ok((
2467 parts.next().unwrap_or_default().parse().unwrap_or(0),
2468 parts.next().unwrap_or_default().parse().unwrap_or(0),
2469 ))
2470 } else {
2471 Ok((0, 0))
2472 }
2473}
2474
2475async fn should_ignore_folder(
2480 context: &Context,
2481 folder: &str,
2482 folder_meaning: FolderMeaning,
2483) -> Result<bool> {
2484 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2485 return Ok(false);
2486 }
2487 if context.is_sentbox(folder).await? {
2488 return Ok(!context.get_config_bool(Config::SentboxWatch).await?);
2490 }
2491 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2492}
2493
2494fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2498 let mut ranges: Vec<UidRange> = vec![];
2500
2501 for ¤t in uids {
2502 if let Some(last) = ranges.last_mut() {
2503 if last.end + 1 == current {
2504 last.end = current;
2505 continue;
2506 }
2507 }
2508
2509 ranges.push(UidRange {
2510 start: current,
2511 end: current,
2512 });
2513 }
2514
2515 let mut result = vec![];
2517 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2518 for range in ranges {
2519 last_uids.reserve((range.end - range.start + 1).try_into()?);
2520 (range.start..=range.end).for_each(|u| last_uids.push(u));
2521 if !last_str.is_empty() {
2522 last_str.push(',');
2523 }
2524 last_str.push_str(&range.to_string());
2525
2526 if last_str.len() > 990 {
2527 result.push((take(&mut last_uids), take(&mut last_str)));
2528 }
2529 }
2530 result.push((last_uids, last_str));
2531
2532 result.retain(|(_, s)| !s.is_empty());
2533 Ok(result)
2534}
2535
2536struct UidRange {
2537 start: u32,
2538 end: u32,
2539 }
2541
2542impl std::fmt::Display for UidRange {
2543 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2544 if self.start == self.end {
2545 write!(f, "{}", self.start)
2546 } else {
2547 write!(f, "{}:{}", self.start, self.end)
2548 }
2549 }
2550}
2551async fn add_all_recipients_as_contacts(
2552 context: &Context,
2553 session: &mut Session,
2554 folder: Config,
2555) -> Result<()> {
2556 let mailbox = if let Some(m) = context.get_config(folder).await? {
2557 m
2558 } else {
2559 info!(
2560 context,
2561 "Folder {} is not configured, skipping fetching contacts from it.", folder
2562 );
2563 return Ok(());
2564 };
2565 let create = false;
2566 let folder_exists = session
2567 .select_with_uidvalidity(context, &mailbox, create)
2568 .await
2569 .with_context(|| format!("could not select {mailbox}"))?;
2570 if !folder_exists {
2571 return Ok(());
2572 }
2573
2574 let recipients = session
2575 .get_all_recipients(context)
2576 .await
2577 .context("could not get recipients")?;
2578
2579 let mut any_modified = false;
2580 for recipient in recipients {
2581 let recipient_addr = match ContactAddress::new(&recipient.addr) {
2582 Err(err) => {
2583 warn!(
2584 context,
2585 "Could not add contact for recipient with address {:?}: {:#}",
2586 recipient.addr,
2587 err
2588 );
2589 continue;
2590 }
2591 Ok(recipient_addr) => recipient_addr,
2592 };
2593
2594 let (_, modified) = Contact::add_or_lookup(
2595 context,
2596 &recipient.display_name.unwrap_or_default(),
2597 &recipient_addr,
2598 Origin::OutgoingTo,
2599 )
2600 .await?;
2601 if modified != Modifier::None {
2602 any_modified = true;
2603 }
2604 }
2605 if any_modified {
2606 context.emit_event(EventType::ContactsChanged(None));
2607 }
2608
2609 Ok(())
2610}
2611
2612#[cfg(test)]
2613mod imap_tests;