diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index f95f0a4cb..0c910909c 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -546,6 +546,9 @@ class Account(object): You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the account is started. + If you are using this from a test, you may want to call + wait_all_initial_fetches() afterwards. + :raises MissingCredentials: if `addr` and `mail_pw` values are not set. :raises ConfigureFailed: if the account could not be configured. diff --git a/python/src/deltachat/direct_imap.py b/python/src/deltachat/direct_imap.py index c4c118486..ca67fd835 100644 --- a/python/src/deltachat/direct_imap.py +++ b/python/src/deltachat/direct_imap.py @@ -24,12 +24,13 @@ def dc_account_extra_configure(account): """ Reset the account (we reuse accounts across tests) and make 'account.direct_imap' available for direct IMAP ops. """ - imap = DirectImap(account) - if imap.select_config_folder("mvbox"): + if not hasattr(account, "direct_imap"): + imap = DirectImap(account) + if imap.select_config_folder("mvbox"): + imap.delete(ALL, expunge=True) + assert imap.select_config_folder("inbox") imap.delete(ALL, expunge=True) - assert imap.select_config_folder("inbox") - imap.delete(ALL, expunge=True) - setattr(account, "direct_imap", imap) + setattr(account, "direct_imap", imap) @deltachat.global_hookimpl diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py index 277d37f7d..20f44a0b9 100644 --- a/python/src/deltachat/events.py +++ b/python/src/deltachat/events.py @@ -1,6 +1,7 @@ import threading import time import re +import os from queue import Queue, Empty import deltachat @@ -48,6 +49,15 @@ class FFIEventLogger: if self.logid: locname += "-" + self.logid s = "{:2.2f} [{}] {}".format(elapsed, locname, message) + + if os.name == "posix": + WARN = '\033[93m' + ERROR = '\033[91m' + ENDC = '\033[0m' + if message.startswith("DC_EVENT_WARNING"): + s = WARN + s + ENDC + if message.startswith("DC_EVENT_ERROR"): + s = ERROR + s + ENDC with self._loglock: print(s, flush=True) @@ -111,6 +121,15 @@ class FFIEventTracker: print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account) break + def wait_all_initial_fetches(self): + """Has to be called after start_io() to wait for fetch_existing_msgs to run + so that new messages are not mistaken for old ones: + - ac1 and ac2 are created + - ac1 sends a message to ac2 + - ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message + - therefore no DC_EVENT_INCOMING_MSG is sent""" + self.get_info_contains("Done fetching existing messages") + def wait_next_incoming_message(self): """ wait for and return next incoming message. """ ev = self.get_matching("DC_EVENT_INCOMING_MSG") diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index 5bf386c6c..f83a417a9 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -342,10 +342,15 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data): mvbox_move=account.get_config("mvbox_move"), sentbox_watch=account.get_config("sentbox_watch"), )) + if hasattr(account, "direct_imap"): + # Attach the existing direct_imap. If we did not do this, a new one would be created and + # delete existing messages (see dc_account_extra_configure(configure)) + ac.direct_imap = account.direct_imap ac._configtracker = ac.configure() return ac def wait_configure_and_start_io(self): + started_accounts = [] for acc in self._accounts: if hasattr(acc, "_configtracker"): acc._configtracker.wait_finish() @@ -353,8 +358,11 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data): acc.set_config("bcc_self", "0") if acc.is_configured() and not acc.is_started(): acc.start_io() + started_accounts.append(acc) print("{}: {} account was successfully setup".format( acc.get_config("displayname"), acc.get_config("addr"))) + for acc in started_accounts: + acc._evtracker.wait_all_initial_fetches() def run_bot_process(self, module, ffi=True): fn = module.__file__ diff --git a/python/tests/test_account.py b/python/tests/test_account.py index b387849cf..a2640fbb1 100644 --- a/python/tests/test_account.py +++ b/python/tests/test_account.py @@ -1644,6 +1644,7 @@ class TestOnlineAccount: chat41 = ac4.create_chat(ac1) chat42 = ac4.create_chat(ac2) ac4.start_io() + ac4._evtracker.wait_all_initial_fetches() lp.sec("ac1: creating group chat with 2 other members") chat = ac1.create_group_chat("title", contacts=[ac2, ac3]) @@ -1923,6 +1924,43 @@ class TestOnlineAccount: assert received_reply.quoted_text == "hello" assert received_reply.quote.id == out_msg.id + @pytest.mark.parametrize("mvbox_move", [False, True]) + def test_add_all_recipients_as_contacts(self, acfactory, lp, mvbox_move): + """Delta Chat reads the recipients from old emails sent by the user and adds them as contacts. + This way, we can already offer them some email addresses they can write to. + + Also test that existing emails are fetched during onboarding. + + Lastly, tests that bcc_self messages moved to the mvbox are marked as read.""" + ac1 = acfactory.get_online_configuring_account(mvbox=mvbox_move, move=mvbox_move) + ac2 = acfactory.get_online_configuring_account() + + acfactory.wait_configure_and_start_io() + + chat = acfactory.get_accepted_chat(ac1, ac2) + + lp.sec("send out message with bcc to ourselves") + if mvbox_move: + ac1.direct_imap.select_config_folder("mvbox") + ac1.direct_imap.idle_start() + ac1.set_config("bcc_self", "1") + chat.send_text("message text") + + # now wait until the bcc_self message arrives + # Also test that bcc_self messages moved to the mvbox are marked as read. + assert ac1.direct_imap.idle_wait_for_seen() + + ac1_clone = acfactory.clone_online_account(ac1) + ac1_clone._configtracker.wait_finish() + ac1_clone.start_io() + + ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED") + ac2_addr = ac2.get_config("addr") + assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts()) + + msg = ac1_clone._evtracker.wait_next_messages_changed() + assert msg.text == "message text" + class TestGroupStressTests: def test_group_many_members_add_leave_remove(self, acfactory, lp): diff --git a/src/config.rs b/src/config.rs index 87d06f966..150718eed 100644 --- a/src/config.rs +++ b/src/config.rs @@ -121,9 +121,9 @@ pub enum Config { #[strum(serialize = "sys.config_keys")] SysConfigKeys, - #[strum(props(default = "0"))] /// Whether we send a warning if the password is wrong (set to false when we send a warning /// because we do not want to send a second warning) + #[strum(props(default = "0"))] NotifyAboutWrongPw, /// address to webrtc instance to use for videochats diff --git a/src/configure/mod.rs b/src/configure/mod.rs index 2187d2f12..32bd3df32 100644 --- a/src/configure/mod.rs +++ b/src/configure/mod.rs @@ -9,11 +9,10 @@ use anyhow::{bail, ensure, Context as _, Result}; use async_std::prelude::*; use async_std::task; use itertools::Itertools; +use job::Action; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use crate::config::Config; -use crate::constants::*; -use crate::context::Context; use crate::dc_tools::*; use crate::imap::Imap; use crate::login_param::{LoginParam, ServerLoginParam}; @@ -23,6 +22,8 @@ use crate::provider::{Protocol, Socket, UsernamePattern}; use crate::smtp::Smtp; use crate::stock::StockMessage; use crate::{chat, e2ee, provider}; +use crate::{constants::*, job}; +use crate::{context::Context, param::Params}; use auto_mozilla::moz_autoconfigure; use auto_outlook::outlk_autodiscover; @@ -349,6 +350,12 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { e2ee::ensure_secret_key_exists(ctx).await?; info!(ctx, "key generation completed"); + job::add( + ctx, + job::Job::new(Action::FetchExistingMsgs, 0, Params::new(), 0), + ) + .await; + progress!(ctx, 940); Ok(()) diff --git a/src/constants.rs b/src/constants.rs index 3c0eb2a33..b8a393520 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -195,6 +195,9 @@ pub const DC_LP_AUTH_NORMAL: i32 = 0x4; /// if none of these flags are set, the default is chosen pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL; +/// How many existing messages shall be fetched after configuration. +pub const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100; + // max. width/height of an avatar pub const AVATAR_SIZE: u32 = 192; diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 6315b9691..d71954055 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -43,6 +43,17 @@ pub async fn dc_receive_imf( server_folder: impl AsRef, server_uid: u32, seen: bool, +) -> Result<()> { + dc_receive_imf_inner(context, imf_raw, server_folder, server_uid, seen, false).await +} + +pub(crate) async fn dc_receive_imf_inner( + context: &Context, + imf_raw: &[u8], + server_folder: impl AsRef, + server_uid: u32, + seen: bool, + fetching_existing_messages: bool, ) -> Result<()> { info!( context, @@ -169,6 +180,7 @@ pub async fn dc_receive_imf( &mut insert_msg_id, &mut created_db_entries, &mut create_event_to_send, + fetching_existing_messages, ) .await { @@ -335,6 +347,7 @@ async fn add_parts( insert_msg_id: &mut MsgId, created_db_entries: &mut Vec<(ChatId, MsgId)>, create_event_to_send: &mut Option, + fetching_existing_messages: bool, ) -> Result<()> { let mut state: MessageState; let mut chat_id_blocked = Blocked::Not; @@ -389,7 +402,7 @@ async fn add_parts( let to_id: u32; if incoming { - state = if seen { + state = if seen || fetching_existing_messages { MessageState::InSeen } else { MessageState::InFresh @@ -532,6 +545,10 @@ async fn add_parts( && show_emails != ShowEmails::All { state = MessageState::InNoticed; + } else if fetching_existing_messages && Blocked::Deaddrop == chat_id_blocked { + // The fetched existing message should be shown in the chatlist-contact-request because + // a new user won't find the contact request in the menu + state = MessageState::InFresh; } } else { // Outgoing @@ -628,6 +645,12 @@ async fn add_parts( } } + if fetching_existing_messages && mime_parser.decrypting_failed { + *chat_id = ChatId::new(DC_CHAT_ID_TRASH); + // We are only gathering old messages on first start. We do not want to add loads of non-decryptable messages to the chats. + info!(context, "Dropping existing non-decipherable message."); + } + // Extract ephemeral timer from the message. let mut ephemeral_timer = if let Some(value) = mime_parser.get(HeaderDef::EphemeralTimer) { match value.parse::() { diff --git a/src/imap/idle.rs b/src/imap/idle.rs index 072c14c98..920ec3eec 100644 --- a/src/imap/idle.rs +++ b/src/imap/idle.rs @@ -160,7 +160,7 @@ impl Imap { // will not find any new. if let Some(ref watch_folder) = watch_folder { - match self.fetch_new_messages(context, watch_folder).await { + match self.fetch_new_messages(context, watch_folder, false).await { Ok(res) => { info!(context, "fetch_new_messages returned {:?}", res); if res { diff --git a/src/imap/mod.rs b/src/imap/mod.rs index ebc846b36..172ddf909 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -3,8 +3,9 @@ //! uses [async-email/async-imap](https://github.com/async-email/async-imap) //! to implement connect, fetch, delete functionality with standard IMAP servers. -use std::collections::BTreeMap; +use std::{cmp, collections::BTreeMap}; +use anyhow::Context as _; use async_imap::{ error::Result as ImapResult, types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute}, @@ -13,12 +14,9 @@ use async_std::prelude::*; use async_std::sync::Receiver; use num_traits::FromPrimitive; -use crate::config::*; use crate::constants::*; use crate::context::Context; -use crate::dc_receive_imf::{ - dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list, -}; +use crate::dc_receive_imf::{from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list}; use crate::error::{bail, format_err, Result}; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; @@ -32,6 +30,7 @@ use crate::provider::{get_provider_info, Socket}; use crate::{ chat, dc_tools::dc_extract_grpid_from_rfc724_mid, scheduler::InterruptInfo, stock::StockMessage, }; +use crate::{config::*, dc_receive_imf::dc_receive_imf_inner}; mod client; mod idle; @@ -40,6 +39,7 @@ mod session; use chat::get_chat_id_by_grpid; use client::Client; +use mailparse::SingleInfo; use message::Message; use session::Session; @@ -448,7 +448,10 @@ impl Imap { } self.setup_handle(context).await?; - while self.fetch_new_messages(context, &watch_folder).await? { + while self + .fetch_new_messages(context, &watch_folder, false) + .await? + { // We fetch until no more new messages are there. } Ok(()) @@ -643,10 +646,11 @@ impl Imap { Ok((new_uid_validity, new_last_seen_uid)) } - async fn fetch_new_messages>( + pub(crate) async fn fetch_new_messages>( &mut self, context: &Context, folder: S, + fetch_existing_msgs: bool, ) -> Result { let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await) .unwrap_or_default(); @@ -655,7 +659,11 @@ impl Imap { .select_with_uidvalidity(context, folder.as_ref()) .await?; - let msgs = self.fetch_after(context, last_seen_uid).await?; + let msgs = if fetch_existing_msgs { + self.fetch_existing_msgs_prefetch().await? + } else { + self.fetch_after(context, last_seen_uid).await? + }; let read_cnt = msgs.len(); let folder: &str = folder.as_ref(); @@ -695,8 +703,9 @@ impl Imap { } // check passed, go fetch the emails - let (new_last_seen_uid_processed, error_cnt) = - self.fetch_many_msgs(context, &folder, &uids).await; + let (new_last_seen_uid_processed, error_cnt) = self + .fetch_many_msgs(context, &folder, &uids, fetch_existing_msgs) + .await; read_errors += error_cnt; // determine which last_seen_uid to use to update to @@ -721,17 +730,66 @@ impl Imap { Ok(read_cnt > 0) } + /// Gets the from, to and bcc addresses from all existing outgoing emails. + pub async fn get_all_recipients(&mut self, context: &Context) -> Result> { + if self.session.is_none() { + bail!("IMAP No Connection established"); + } + + let session = self.session.as_mut().unwrap(); + let self_addr = context + .get_config(Config::ConfiguredAddr) + .await + .ok_or_else(|| format_err!("Not configured"))?; + + let search_command = format!("FROM \"{}\"", self_addr); + let uids = session.uid_search(search_command).await?; + let uid_strings: Vec = uids.into_iter().map(|s| s.to_string()).collect(); + + let mut result = Vec::new(); + // We fetch the emails in chunks of 100 because according to https://tools.ietf.org/html/rfc2683#section-3.2.1.5 + // command lines should not be much more than 1000 chars and UIDs can get up to 9- or 10-digit + // (servers should allow at least 8000 chars) + for uid_chunk in uid_strings.chunks(100) { + let uid_set = uid_chunk.join(","); + + let mut list = session + .uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])") + .await + .map_err(|err| { + format_err!("IMAP Could not fetch (get_all_recipients()): {}", err) + })?; + + while let Some(fetch) = list.next().await { + let msg = fetch?; + match get_fetch_headers(&msg) { + Ok(headers) => { + let (from_id, _, _) = + from_field_to_contact_id(context, &mimeparser::get_from(&headers)) + .await?; + if from_id == DC_CONTACT_ID_SELF { + result.extend(mimeparser::get_recipients(&headers)); + } + } + + Err(err) => { + warn!(context, "{}", err); + continue; + } + }; + } + } + Ok(result) + } + /// Fetch all uids larger than the passed in. Returns a sorted list of fetch results. async fn fetch_after( &mut self, context: &Context, uid: u32, ) -> Result> { - if self.session.is_none() { - bail!("IMAP No Connection established"); - } - - let session = self.session.as_mut().unwrap(); + let session = self.session.as_mut(); + let session = session.context("fetch_after(): IMAP No Connection established")?; // fetch messages with larger UID than the last one seen // `(UID FETCH lastseenuid+1:*)`, see RFC 4549 @@ -769,6 +827,40 @@ impl Imap { Ok(new_msgs) } + /// Like fetch_after(), but not for new messages but existing ones (the DC_FETCH_EXISTING_MSGS_COUNT newest messages) + async fn fetch_existing_msgs_prefetch( + &mut self, + ) -> Result> { + let exists: i64 = { + let mailbox = self.config.selected_mailbox.as_ref(); + let mailbox = mailbox.context("fetch_existing_msgs_prefetch(): no mailbox selected")?; + mailbox.exists.into() + }; + let session = self.session.as_mut(); + let session = + session.context("fetch_existing_msgs_prefetch(): IMAP No Connection established")?; + + // Fetch last DC_FETCH_EXISTING_MSGS_COUNT (100) messages. + // Sequence numbers are sequential. If there are 1000 messages in the inbox, + // we can fetch the sequence numbers 900-1000 and get the last 100 messages. + let first = cmp::max(1, exists - DC_FETCH_EXISTING_MSGS_COUNT); + let set = format!("{}:*", first); + let mut list = session + .fetch(&set, PREFETCH_FLAGS) + .await + .map_err(|err| format_err!("IMAP Could not fetch: {}", err))?; + + let mut msgs = BTreeMap::new(); + while let Some(fetch) = list.next().await { + let msg = fetch?; + if let Some(msg_uid) = msg.uid { + msgs.insert(msg_uid, msg); + } + } + + Ok(msgs) + } + async fn set_config_last_seen_uid>( &self, context: &Context, @@ -795,6 +887,7 @@ impl Imap { context: &Context, folder: S, server_uids: &[u32], + fetching_existing_messages: bool, ) -> (Option, usize) { let set = match server_uids { [] => return (None, 0), @@ -868,7 +961,16 @@ impl Imap { let body = msg.body().unwrap(); let is_seen = msg.flags().any(|flag| flag == Flag::Seen); - match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await { + match dc_receive_imf_inner( + &context, + &body, + &folder, + server_uid, + is_seen, + fetching_existing_messages, + ) + .await + { Ok(_) => last_uid = Some(server_uid), Err(err) => { warn!(context, "dc_receive_imf error: {}", err); @@ -1457,17 +1559,16 @@ async fn precheck_imf( if old_server_folder != server_folder || old_server_uid != server_uid { update_server_uid(context, rfc724_mid, server_folder, server_uid).await; - if let Ok(MessageState::InSeen) = msg_id.get_state(context).await { - job::add( - context, - job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0), - ) - .await; - }; - context - .interrupt_inbox(InterruptInfo::new(false, Some(msg_id))) - .await; - info!(context, "Updating server_uid and interrupting") + if let Ok(message_state) = msg_id.get_state(context).await { + if message_state == MessageState::InSeen || message_state.is_outgoing() { + job::add( + context, + job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0), + ) + .await; + } + } + info!(context, "Updating server_uid and adding markseen job"); } Ok(true) } else { diff --git a/src/job.rs b/src/job.rs index 0baac97b8..455c6ba06 100644 --- a/src/job.rs +++ b/src/job.rs @@ -14,7 +14,6 @@ use async_smtp::smtp::response::Category; use async_smtp::smtp::response::Code; use async_smtp::smtp::response::Detail; -use crate::blob::BlobObject; use crate::chat::{self, ChatId}; use crate::config::Config; use crate::contact::Contact; @@ -30,6 +29,7 @@ use crate::message::{self, Message, MessageState}; use crate::mimefactory::MimeFactory; use crate::param::*; use crate::smtp::Smtp; +use crate::{blob::BlobObject, contact::normalize_name, contact::Modifier, contact::Origin}; use crate::{scheduler::InterruptInfo, sql}; // results in ~3 weeks for the last backoff timespan @@ -92,6 +92,7 @@ pub enum Action { // Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999 Housekeeping = 105, // low priority ... + FetchExistingMsgs = 110, MarkseenMsgOnImap = 130, // Moving message is prioritized lower than deletion so we don't @@ -124,6 +125,7 @@ impl From for Thread { Unknown => Thread::Unknown, Housekeeping => Thread::Imap, + FetchExistingMsgs => Thread::Imap, DeleteMsgOnImap => Thread::Imap, ResyncFolders => Thread::Imap, MarkseenMsgOnImap => Thread::Imap, @@ -619,6 +621,38 @@ impl Job { } } + /// Read the recipients from old emails sent by the user and add them as contacts. + /// This way, we can already offer them some email addresses they can write to. + /// + /// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server + /// and show them in the chat list. + async fn fetch_existing_msgs(&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; + } + + add_all_recipients_as_contacts(context, imap, Config::ConfiguredSentboxFolder).await; + add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await; + add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await; + + for config in &[ + Config::ConfiguredMvboxFolder, + Config::ConfiguredInboxFolder, + Config::ConfiguredSentboxFolder, + ] { + if let Some(folder) = context.get_config(*config).await { + if let Err(e) = imap.fetch_new_messages(context, folder, true).await { + // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}: + warn!(context, "Could not fetch messages, retrying: {:#}", e); + return Status::RetryLater; + }; + } + } + info!(context, "Done fetching existing messages."); + Status::Finished(Ok(())) + } + /// Synchronizes UIDs for sentbox, inbox and mvbox, in this order. /// /// If a copy of the message is present in multiple folders, mvbox @@ -759,6 +793,50 @@ async fn set_delivered(context: &Context, msg_id: MsgId) { context.emit_event(EventType::MsgDelivered { chat_id, msg_id }); } +async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) { + let mailbox = if let Some(m) = context.get_config(folder).await { + m + } else { + return; + }; + if let Err(e) = imap.select_with_uidvalidity(context, &mailbox).await { + warn!(context, "Could not select {}: {}", mailbox, e); + return; + } + match imap.get_all_recipients(context).await { + Ok(contacts) => { + let mut any_modified = false; + for contact in contacts { + let display_name_normalized = contact + .display_name + .as_ref() + .map(normalize_name) + .unwrap_or_default(); + + match Contact::add_or_lookup( + context, + display_name_normalized, + contact.addr, + Origin::OutgoingTo, + ) + .await + { + Ok((_, modified)) => { + if modified != Modifier::None { + any_modified = true; + } + } + Err(e) => warn!(context, "Could not add recipient: {}", e), + } + } + if any_modified { + context.emit_event(EventType::ContactsChanged(None)); + } + } + Err(e) => warn!(context, "Could not add recipients: {}", e), + }; +} + /// Constructs a job for sending a message. /// /// Returns `None` if no messages need to be sent out. @@ -1007,6 +1085,7 @@ async fn perform_job_action( 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::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await, Action::Housekeeping => { sql::housekeeping(context).await; Status::Finished(Ok(())) @@ -1072,6 +1151,7 @@ pub async fn add(context: &Context, job: Job) { | Action::DeleteMsgOnImap | Action::ResyncFolders | Action::MarkseenMsgOnImap + | Action::FetchExistingMsgs | Action::MoveMsg => { info!(context, "interrupt: imap"); context diff --git a/src/message.rs b/src/message.rs index dbefd58ad..9dbc08206 100644 --- a/src/message.rs +++ b/src/message.rs @@ -920,12 +920,17 @@ impl From for LotState { impl MessageState { pub fn can_fail(self) -> bool { + use MessageState::*; matches!( self, - MessageState::OutPreparing - | MessageState::OutPending - | MessageState::OutDelivered - | MessageState::OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed. + OutPreparing | OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed. + ) + } + pub fn is_outgoing(self) -> bool { + use MessageState::*; + matches!( + self, + OutPreparing | OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd ) } } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index dff6d16b4..933fc11b0 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1286,9 +1286,9 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result Vec { +pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec { get_all_addresses_from_header(headers, |header_key| { - header_key == "to" || header_key == "cc" + header_key == "to" || header_key == "cc" || header_key == "bcc" }) }