deltachat/
context.rs

1//! Context module.
2
3use std::collections::{BTreeMap, HashMap};
4use std::ffi::OsString;
5use std::ops::Deref;
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, OnceLock};
9use std::time::Duration;
10
11use anyhow::{Context as _, Result, bail, ensure};
12use async_channel::{self as channel, Receiver, Sender};
13use pgp::types::PublicKeyTrait;
14use ratelimit::Ratelimit;
15use tokio::sync::{Mutex, Notify, RwLock};
16
17use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
18use crate::chatlist_events;
19use crate::config::Config;
20use crate::constants::{
21    self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
22};
23use crate::contact::{Contact, ContactId, import_vcard, mark_contact_id_as_verified};
24use crate::debug_logging::DebugLogging;
25use crate::download::DownloadState;
26use crate::events::{Event, EventEmitter, EventType, Events};
27use crate::imap::{FolderMeaning, Imap, ServerMetadata};
28use crate::key::{load_self_secret_key, self_fingerprint};
29use crate::log::{info, warn};
30use crate::logged_debug_assert;
31use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
32use crate::message::{self, Message, MessageState, MsgId};
33use crate::param::{Param, Params};
34use crate::peer_channels::Iroh;
35use crate::push::PushSubscriber;
36use crate::quota::QuotaInfo;
37use crate::scheduler::{ConnectivityStore, SchedulerState, convert_folder_meaning};
38use crate::sql::Sql;
39use crate::stock_str::StockStrings;
40use crate::timesmearing::SmearedTimestamp;
41use crate::tools::{self, create_id, duration_to_str, time, time_elapsed};
42
43/// Builder for the [`Context`].
44///
45/// Many arguments to the [`Context`] are kind of optional and only needed to handle
46/// multiple contexts, for which the [account manager](crate::accounts::Accounts) should be
47/// used.  This builder makes creating a new context simpler, especially for the
48/// standalone-context case.
49///
50/// # Examples
51///
52/// Creating a new unencrypted database:
53///
54/// ```
55/// # let rt = tokio::runtime::Runtime::new().unwrap();
56/// # rt.block_on(async move {
57/// use deltachat::context::ContextBuilder;
58///
59/// let dir = tempfile::tempdir().unwrap();
60/// let context = ContextBuilder::new(dir.path().join("db"))
61///      .open()
62///      .await
63///      .unwrap();
64/// drop(context);
65/// # });
66/// ```
67///
68/// To use an encrypted database provide a password.  If the database does not yet exist it
69/// will be created:
70///
71/// ```
72/// # let rt = tokio::runtime::Runtime::new().unwrap();
73/// # rt.block_on(async move {
74/// use deltachat::context::ContextBuilder;
75///
76/// let dir = tempfile::tempdir().unwrap();
77/// let context = ContextBuilder::new(dir.path().join("db"))
78///      .with_password("secret".into())
79///      .open()
80///      .await
81///      .unwrap();
82/// drop(context);
83/// # });
84/// ```
85#[derive(Clone, Debug)]
86pub struct ContextBuilder {
87    dbfile: PathBuf,
88    id: u32,
89    events: Events,
90    stock_strings: StockStrings,
91    password: Option<String>,
92
93    push_subscriber: Option<PushSubscriber>,
94}
95
96impl ContextBuilder {
97    /// Create the builder using the given database file.
98    ///
99    /// The *dbfile* should be in a dedicated directory and this directory must exist.  The
100    /// [`Context`] will create other files and folders in the same directory as the
101    /// database file used.
102    pub fn new(dbfile: PathBuf) -> Self {
103        ContextBuilder {
104            dbfile,
105            id: rand::random(),
106            events: Events::new(),
107            stock_strings: StockStrings::new(),
108            password: None,
109            push_subscriber: None,
110        }
111    }
112
113    /// Sets the context ID.
114    ///
115    /// This identifier is used e.g. in [`Event`]s to identify which [`Context`] an event
116    /// belongs to.  The only real limit on it is that it should not conflict with any other
117    /// [`Context`]s you currently have open.  So if you handle multiple [`Context`]s you
118    /// may want to use this.
119    ///
120    /// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
121    /// common case for using multiple [`Context`] instances.
122    pub fn with_id(mut self, id: u32) -> Self {
123        self.id = id;
124        self
125    }
126
127    /// Sets the event channel for this [`Context`].
128    ///
129    /// Mostly useful when using multiple [`Context`]s, this allows creating one [`Events`]
130    /// channel and passing it to all [`Context`]s so all events are received on the same
131    /// channel.
132    ///
133    /// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
134    /// common case for using multiple [`Context`] instances.
135    pub fn with_events(mut self, events: Events) -> Self {
136        self.events = events;
137        self
138    }
139
140    /// Sets the [`StockStrings`] map to use for this [`Context`].
141    ///
142    /// This is useful in order to share the same translation strings in all [`Context`]s.
143    /// The mapping may be empty when set, it will be populated by
144    /// [`Context::set_stock-translation`] or [`Accounts::set_stock_translation`] calls.
145    ///
146    /// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
147    /// common case for using multiple [`Context`] instances.
148    ///
149    /// [`Accounts::set_stock_translation`]: crate::accounts::Accounts::set_stock_translation
150    pub fn with_stock_strings(mut self, stock_strings: StockStrings) -> Self {
151        self.stock_strings = stock_strings;
152        self
153    }
154
155    /// Sets the password to unlock the database.
156    ///
157    /// If an encrypted database is used it must be opened with a password.  Setting a
158    /// password on a new database will enable encryption.
159    pub fn with_password(mut self, password: String) -> Self {
160        self.password = Some(password);
161        self
162    }
163
164    /// Sets push subscriber.
165    pub(crate) fn with_push_subscriber(mut self, push_subscriber: PushSubscriber) -> Self {
166        self.push_subscriber = Some(push_subscriber);
167        self
168    }
169
170    /// Builds the [`Context`] without opening it.
171    pub async fn build(self) -> Result<Context> {
172        let push_subscriber = self.push_subscriber.unwrap_or_default();
173        let context = Context::new_closed(
174            &self.dbfile,
175            self.id,
176            self.events,
177            self.stock_strings,
178            push_subscriber,
179        )
180        .await?;
181        Ok(context)
182    }
183
184    /// Builds the [`Context`] and opens it.
185    ///
186    /// Returns error if context cannot be opened with the given passphrase.
187    pub async fn open(self) -> Result<Context> {
188        let password = self.password.clone().unwrap_or_default();
189        let context = self.build().await?;
190        match context.open(password).await? {
191            true => Ok(context),
192            false => bail!("database could not be decrypted, incorrect or missing password"),
193        }
194    }
195}
196
197/// The context for a single DeltaChat account.
198///
199/// This contains all the state for a single DeltaChat account, including background tasks
200/// running in Tokio to operate the account.  The [`Context`] can be cheaply cloned.
201///
202/// Each context, and thus each account, must be associated with an directory where all the
203/// state is kept.  This state is also preserved between restarts.
204///
205/// To use multiple accounts it is best to look at the [accounts
206/// manager][crate::accounts::Accounts] which handles storing multiple accounts in a single
207/// directory structure and handles loading them all concurrently.
208#[derive(Clone, Debug)]
209pub struct Context {
210    pub(crate) inner: Arc<InnerContext>,
211}
212
213impl Deref for Context {
214    type Target = InnerContext;
215
216    fn deref(&self) -> &Self::Target {
217        &self.inner
218    }
219}
220
221/// Actual context, expensive to clone.
222#[derive(Debug)]
223pub struct InnerContext {
224    /// Blob directory path
225    pub(crate) blobdir: PathBuf,
226    pub(crate) sql: Sql,
227    pub(crate) smeared_timestamp: SmearedTimestamp,
228    /// The global "ongoing" process state.
229    ///
230    /// This is a global mutex-like state for operations which should be modal in the
231    /// clients.
232    running_state: RwLock<RunningState>,
233    /// Mutex to avoid generating the key for the user more than once.
234    pub(crate) generating_key_mutex: Mutex<()>,
235    /// Mutex to enforce only a single running oauth2 is running.
236    pub(crate) oauth2_mutex: Mutex<()>,
237    /// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messages being sent.
238    pub(crate) wrong_pw_warning_mutex: Mutex<()>,
239    pub(crate) translated_stockstrings: StockStrings,
240    pub(crate) events: Events,
241
242    pub(crate) scheduler: SchedulerState,
243    pub(crate) ratelimit: RwLock<Ratelimit>,
244
245    /// Recently loaded quota information, if any.
246    /// Set to `None` if quota was never tried to load.
247    pub(crate) quota: RwLock<Option<QuotaInfo>>,
248
249    /// IMAP UID resync request.
250    pub(crate) resync_request: AtomicBool,
251
252    /// Notify about new messages.
253    ///
254    /// This causes [`Context::wait_next_msgs`] to wake up.
255    pub(crate) new_msgs_notify: Notify,
256
257    /// Server ID response if ID capability is supported
258    /// and the server returned non-NIL on the inbox connection.
259    /// <https://datatracker.ietf.org/doc/html/rfc2971>
260    pub(crate) server_id: RwLock<Option<HashMap<String, String>>>,
261
262    /// IMAP METADATA.
263    pub(crate) metadata: RwLock<Option<ServerMetadata>>,
264
265    pub(crate) last_full_folder_scan: Mutex<Option<tools::Time>>,
266
267    /// ID for this `Context` in the current process.
268    ///
269    /// This allows for multiple `Context`s open in a single process where each context can
270    /// be identified by this ID.
271    pub(crate) id: u32,
272
273    creation_time: tools::Time,
274
275    /// The text of the last error logged and emitted as an event.
276    /// If the ui wants to display an error after a failure,
277    /// `last_error` should be used to avoid races with the event thread.
278    pub(crate) last_error: parking_lot::RwLock<String>,
279
280    /// It's not possible to emit migration errors as an event,
281    /// because at the time of the migration, there is no event emitter yet.
282    /// So, this holds the error that happened during migration, if any.
283    /// This is necessary for the possibly-failible PGP migration,
284    /// which happened 2025-05, and can be removed a few releases later.
285    pub(crate) migration_error: parking_lot::RwLock<Option<String>>,
286
287    /// If debug logging is enabled, this contains all necessary information
288    ///
289    /// Standard RwLock instead of [`tokio::sync::RwLock`] is used
290    /// because the lock is used from synchronous [`Context::emit_event`].
291    pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,
292
293    /// Push subscriber to store device token
294    /// and register for heartbeat notifications.
295    pub(crate) push_subscriber: PushSubscriber,
296
297    /// True if account has subscribed to push notifications via IMAP.
298    pub(crate) push_subscribed: AtomicBool,
299
300    /// Iroh for realtime peer channels.
301    pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
302
303    /// The own fingerprint, if it was computed already.
304    /// tokio::sync::OnceCell would be possible to use, but overkill for our usecase;
305    /// the standard library's OnceLock is enough, and it's a lot smaller in memory.
306    pub(crate) self_fingerprint: OnceLock<String>,
307
308    /// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity,
309    /// see [`Context::get_connectivity()`].
310    pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
311}
312
313/// The state of ongoing process.
314#[derive(Debug)]
315enum RunningState {
316    /// Ongoing process is allocated.
317    Running { cancel_sender: Sender<()> },
318
319    /// Cancel signal has been sent, waiting for ongoing process to be freed.
320    ShallStop { request: tools::Time },
321
322    /// There is no ongoing process, a new one can be allocated.
323    Stopped,
324}
325
326impl Default for RunningState {
327    fn default() -> Self {
328        Self::Stopped
329    }
330}
331
332/// Return some info about deltachat-core
333///
334/// This contains information mostly about the library itself, the
335/// actual keys and their values which will be present are not
336/// guaranteed.  Calling [Context::get_info] also includes information
337/// about the context on top of the information here.
338pub fn get_info() -> BTreeMap<&'static str, String> {
339    let mut res = BTreeMap::new();
340
341    #[cfg(debug_assertions)]
342    res.insert(
343        "debug_assertions",
344        "On - DO NOT RELEASE THIS BUILD".to_string(),
345    );
346    #[cfg(not(debug_assertions))]
347    res.insert("debug_assertions", "Off".to_string());
348
349    res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
350    res.insert("sqlite_version", rusqlite::version().to_string());
351    res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
352    res.insert("num_cpus", num_cpus::get().to_string());
353    res.insert("level", "awesome".into());
354    res
355}
356
357impl Context {
358    /// Creates new context and opens the database.
359    pub async fn new(
360        dbfile: &Path,
361        id: u32,
362        events: Events,
363        stock_strings: StockStrings,
364    ) -> Result<Context> {
365        let context =
366            Self::new_closed(dbfile, id, events, stock_strings, Default::default()).await?;
367
368        // Open the database if is not encrypted.
369        if context.check_passphrase("".to_string()).await? {
370            context.sql.open(&context, "".to_string()).await?;
371        }
372        Ok(context)
373    }
374
375    /// Creates new context without opening the database.
376    pub async fn new_closed(
377        dbfile: &Path,
378        id: u32,
379        events: Events,
380        stockstrings: StockStrings,
381        push_subscriber: PushSubscriber,
382    ) -> Result<Context> {
383        let mut blob_fname = OsString::new();
384        blob_fname.push(dbfile.file_name().unwrap_or_default());
385        blob_fname.push("-blobs");
386        let blobdir = dbfile.with_file_name(blob_fname);
387        if !blobdir.exists() {
388            tokio::fs::create_dir_all(&blobdir).await?;
389        }
390        let context = Context::with_blobdir(
391            dbfile.into(),
392            blobdir,
393            id,
394            events,
395            stockstrings,
396            push_subscriber,
397        )?;
398        Ok(context)
399    }
400
401    /// Opens the database with the given passphrase.
402    ///
403    /// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
404    /// errors.
405    pub async fn open(&self, passphrase: String) -> Result<bool> {
406        if self.sql.check_passphrase(passphrase.clone()).await? {
407            self.sql.open(self, passphrase).await?;
408            Ok(true)
409        } else {
410            Ok(false)
411        }
412    }
413
414    /// Changes encrypted database passphrase.
415    pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
416        self.sql.change_passphrase(passphrase).await?;
417        Ok(())
418    }
419
420    /// Returns true if database is open.
421    pub async fn is_open(&self) -> bool {
422        self.sql.is_open().await
423    }
424
425    /// Tests the database passphrase.
426    ///
427    /// Returns true if passphrase is correct.
428    ///
429    /// Fails if database is already open.
430    pub(crate) async fn check_passphrase(&self, passphrase: String) -> Result<bool> {
431        self.sql.check_passphrase(passphrase).await
432    }
433
434    pub(crate) fn with_blobdir(
435        dbfile: PathBuf,
436        blobdir: PathBuf,
437        id: u32,
438        events: Events,
439        stockstrings: StockStrings,
440        push_subscriber: PushSubscriber,
441    ) -> Result<Context> {
442        ensure!(
443            blobdir.is_dir(),
444            "Blobdir does not exist: {}",
445            blobdir.display()
446        );
447
448        let new_msgs_notify = Notify::new();
449        // Notify once immediately to allow processing old messages
450        // without starting I/O.
451        new_msgs_notify.notify_one();
452
453        let inner = InnerContext {
454            id,
455            blobdir,
456            running_state: RwLock::new(Default::default()),
457            sql: Sql::new(dbfile),
458            smeared_timestamp: SmearedTimestamp::new(),
459            generating_key_mutex: Mutex::new(()),
460            oauth2_mutex: Mutex::new(()),
461            wrong_pw_warning_mutex: Mutex::new(()),
462            translated_stockstrings: stockstrings,
463            events,
464            scheduler: SchedulerState::new(),
465            ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
466            quota: RwLock::new(None),
467            resync_request: AtomicBool::new(false),
468            new_msgs_notify,
469            server_id: RwLock::new(None),
470            metadata: RwLock::new(None),
471            creation_time: tools::Time::now(),
472            last_full_folder_scan: Mutex::new(None),
473            last_error: parking_lot::RwLock::new("".to_string()),
474            migration_error: parking_lot::RwLock::new(None),
475            debug_logging: std::sync::RwLock::new(None),
476            push_subscriber,
477            push_subscribed: AtomicBool::new(false),
478            iroh: Arc::new(RwLock::new(None)),
479            self_fingerprint: OnceLock::new(),
480            connectivities: parking_lot::Mutex::new(Vec::new()),
481        };
482
483        let ctx = Context {
484            inner: Arc::new(inner),
485        };
486
487        Ok(ctx)
488    }
489
490    /// Starts the IO scheduler.
491    pub async fn start_io(&self) {
492        if !self.is_configured().await.unwrap_or_default() {
493            warn!(self, "can not start io on a context that is not configured");
494            return;
495        }
496
497        if self.is_chatmail().await.unwrap_or_default() {
498            let mut lock = self.ratelimit.write().await;
499            // Allow at least 1 message every second + a burst of 3.
500            *lock = Ratelimit::new(Duration::new(3, 0), 3.0);
501        }
502
503        // The next line is mainly for iOS:
504        // iOS starts a separate process for receiving notifications and if the user concurrently
505        // starts the app, the UI process opens the database but waits with calling start_io()
506        // until the notifications process finishes.
507        // Now, some configs may have changed, so, we need to invalidate the cache.
508        self.sql.config_cache.write().await.clear();
509
510        self.scheduler.start(self).await;
511    }
512
513    /// Stops the IO scheduler.
514    pub async fn stop_io(&self) {
515        self.scheduler.stop(self).await;
516        if let Some(iroh) = self.iroh.write().await.take() {
517            // Close all QUIC connections.
518
519            // Spawn into a separate task,
520            // because Iroh calls `wait_idle()` internally
521            // and it may take time, especially if the network
522            // has become unavailable.
523            tokio::spawn(async move {
524                // We do not log the error because we do not want the task
525                // to hold the reference to Context.
526                let _ = tokio::time::timeout(Duration::from_secs(60), iroh.close()).await;
527            });
528        }
529    }
530
531    /// Restarts the IO scheduler if it was running before
532    /// when it is not running this is an no-op
533    pub async fn restart_io_if_running(&self) {
534        self.scheduler.restart(self).await;
535    }
536
537    /// Indicate that the network likely has come back.
538    pub async fn maybe_network(&self) {
539        if let Some(ref iroh) = *self.iroh.read().await {
540            iroh.network_change().await;
541        }
542        self.scheduler.maybe_network().await;
543    }
544
545    /// Returns true if an account is on a chatmail server.
546    pub async fn is_chatmail(&self) -> Result<bool> {
547        self.get_config_bool(Config::IsChatmail).await
548    }
549
550    /// Returns maximum number of recipients the provider allows to send a single email to.
551    pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> {
552        let is_chatmail = self.is_chatmail().await?;
553        let val = self
554            .get_configured_provider()
555            .await?
556            .and_then(|provider| provider.opt.max_smtp_rcpt_to)
557            .map_or_else(
558                || match is_chatmail {
559                    true => usize::MAX,
560                    false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
561                },
562                usize::from,
563            );
564        Ok(val)
565    }
566
567    /// Does a single round of fetching from IMAP and returns.
568    ///
569    /// Can be used even if I/O is currently stopped.
570    /// If I/O is currently stopped, starts a new IMAP connection
571    /// and fetches from Inbox and DeltaChat folders.
572    pub async fn background_fetch(&self) -> Result<()> {
573        if !(self.is_configured().await?) {
574            return Ok(());
575        }
576
577        let address = self.get_primary_self_addr().await?;
578        let time_start = tools::Time::now();
579        info!(self, "background_fetch started fetching {address}.");
580
581        if self.scheduler.is_running().await {
582            self.scheduler.maybe_network().await;
583            self.wait_for_all_work_done().await;
584        } else {
585            // Pause the scheduler to ensure another connection does not start
586            // while we are fetching on a dedicated connection.
587            let _pause_guard = self.scheduler.pause(self).await?;
588
589            // Start a new dedicated connection.
590            let mut connection = Imap::new_configured(self, channel::bounded(1).1).await?;
591            let mut session = connection.prepare(self).await?;
592
593            // Fetch IMAP folders.
594            // Inbox is fetched before Mvbox because fetching from Inbox
595            // may result in moving some messages to Mvbox.
596            for folder_meaning in [FolderMeaning::Inbox, FolderMeaning::Mvbox] {
597                if let Some((_folder_config, watch_folder)) =
598                    convert_folder_meaning(self, folder_meaning).await?
599                {
600                    connection
601                        .fetch_move_delete(self, &mut session, &watch_folder, folder_meaning)
602                        .await?;
603                }
604            }
605
606            // Update quota (to send warning if full) - but only check it once in a while.
607            if self
608                .quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
609                .await
610            {
611                if let Err(err) = self.update_recent_quota(&mut session).await {
612                    warn!(self, "Failed to update quota: {err:#}.");
613                }
614            }
615        }
616
617        info!(
618            self,
619            "background_fetch done for {address} took {:?}.",
620            time_elapsed(&time_start),
621        );
622
623        Ok(())
624    }
625
626    pub(crate) async fn schedule_resync(&self) -> Result<()> {
627        self.resync_request.store(true, Ordering::Relaxed);
628        self.scheduler.interrupt_inbox().await;
629        Ok(())
630    }
631
632    /// Returns a reference to the underlying SQL instance.
633    ///
634    /// Warning: this is only here for testing, not part of the public API.
635    #[cfg(feature = "internals")]
636    pub fn sql(&self) -> &Sql {
637        &self.inner.sql
638    }
639
640    /// Returns database file path.
641    pub fn get_dbfile(&self) -> &Path {
642        self.sql.dbfile.as_path()
643    }
644
645    /// Returns blob directory path.
646    pub fn get_blobdir(&self) -> &Path {
647        self.blobdir.as_path()
648    }
649
650    /// Emits a single event.
651    pub fn emit_event(&self, event: EventType) {
652        {
653            let lock = self.debug_logging.read().expect("RwLock is poisoned");
654            if let Some(debug_logging) = &*lock {
655                debug_logging.log_event(event.clone());
656            }
657        }
658        self.events.emit(Event {
659            id: self.id,
660            typ: event,
661        });
662    }
663
664    /// Emits a generic MsgsChanged event (without chat or message id)
665    pub fn emit_msgs_changed_without_ids(&self) {
666        self.emit_event(EventType::MsgsChanged {
667            chat_id: ChatId::new(0),
668            msg_id: MsgId::new(0),
669        });
670    }
671
672    /// Emits a MsgsChanged event with specified chat and message ids
673    ///
674    /// If IDs are unset, [`Self::emit_msgs_changed_without_ids`]
675    /// or [`Self::emit_msgs_changed_without_msg_id`] should be used
676    /// instead of this function.
677    pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
678        logged_debug_assert!(
679            self,
680            !chat_id.is_unset(),
681            "emit_msgs_changed: chat_id is unset."
682        );
683        logged_debug_assert!(
684            self,
685            !msg_id.is_unset(),
686            "emit_msgs_changed: msg_id is unset."
687        );
688
689        self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
690        chatlist_events::emit_chatlist_changed(self);
691        chatlist_events::emit_chatlist_item_changed(self, chat_id);
692    }
693
694    /// Emits a MsgsChanged event with specified chat and without message id.
695    pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) {
696        logged_debug_assert!(
697            self,
698            !chat_id.is_unset(),
699            "emit_msgs_changed_without_msg_id: chat_id is unset."
700        );
701
702        self.emit_event(EventType::MsgsChanged {
703            chat_id,
704            msg_id: MsgId::new(0),
705        });
706        chatlist_events::emit_chatlist_changed(self);
707        chatlist_events::emit_chatlist_item_changed(self, chat_id);
708    }
709
710    /// Emits an IncomingMsg event with specified chat and message ids
711    pub fn emit_incoming_msg(&self, chat_id: ChatId, msg_id: MsgId) {
712        debug_assert!(!chat_id.is_unset());
713        debug_assert!(!msg_id.is_unset());
714
715        self.emit_event(EventType::IncomingMsg { chat_id, msg_id });
716        chatlist_events::emit_chatlist_changed(self);
717        chatlist_events::emit_chatlist_item_changed(self, chat_id);
718    }
719
720    /// Emits an LocationChanged event and a WebxdcStatusUpdate in case there is a maps integration
721    pub async fn emit_location_changed(&self, contact_id: Option<ContactId>) -> Result<()> {
722        self.emit_event(EventType::LocationChanged(contact_id));
723
724        if let Some(msg_id) = self
725            .get_config_parsed::<u32>(Config::WebxdcIntegration)
726            .await?
727        {
728            self.emit_event(EventType::WebxdcStatusUpdate {
729                msg_id: MsgId::new(msg_id),
730                status_update_serial: Default::default(),
731            })
732        }
733
734        Ok(())
735    }
736
737    /// Returns a receiver for emitted events.
738    ///
739    /// Multiple emitters can be created, but note that in this case each emitted event will
740    /// only be received by one of the emitters, not by all of them.
741    pub fn get_event_emitter(&self) -> EventEmitter {
742        self.events.get_emitter()
743    }
744
745    /// Get the ID of this context.
746    pub fn get_id(&self) -> u32 {
747        self.id
748    }
749
750    // Ongoing process allocation/free/check
751
752    /// Tries to acquire the global UI "ongoing" mutex.
753    ///
754    /// This is for modal operations during which no other user actions are allowed.  Only
755    /// one such operation is allowed at any given time.
756    ///
757    /// The return value is a cancel token, which will release the ongoing mutex when
758    /// dropped.
759    pub(crate) async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
760        let mut s = self.running_state.write().await;
761        ensure!(
762            matches!(*s, RunningState::Stopped),
763            "There is already another ongoing process running."
764        );
765
766        let (sender, receiver) = channel::bounded(1);
767        *s = RunningState::Running {
768            cancel_sender: sender,
769        };
770
771        Ok(receiver)
772    }
773
774    pub(crate) async fn free_ongoing(&self) {
775        let mut s = self.running_state.write().await;
776        if let RunningState::ShallStop { request } = *s {
777            info!(self, "Ongoing stopped in {:?}", time_elapsed(&request));
778        }
779        *s = RunningState::Stopped;
780    }
781
782    /// Signal an ongoing process to stop.
783    pub async fn stop_ongoing(&self) {
784        let mut s = self.running_state.write().await;
785        match &*s {
786            RunningState::Running { cancel_sender } => {
787                if let Err(err) = cancel_sender.send(()).await {
788                    warn!(self, "could not cancel ongoing: {:#}", err);
789                }
790                info!(self, "Signaling the ongoing process to stop ASAP.",);
791                *s = RunningState::ShallStop {
792                    request: tools::Time::now(),
793                };
794            }
795            RunningState::ShallStop { .. } | RunningState::Stopped => {
796                info!(self, "No ongoing process to stop.",);
797            }
798        }
799    }
800
801    #[allow(unused)]
802    pub(crate) async fn shall_stop_ongoing(&self) -> bool {
803        match &*self.running_state.read().await {
804            RunningState::Running { .. } => false,
805            RunningState::ShallStop { .. } | RunningState::Stopped => true,
806        }
807    }
808
809    /*******************************************************************************
810     * UI chat/message related API
811     ******************************************************************************/
812
813    /// Returns information about the context as key-value pairs.
814    pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
815        let l = EnteredLoginParam::load(self).await?;
816        let l2 = ConfiguredLoginParam::load(self)
817            .await?
818            .map_or_else(|| "Not configured".to_string(), |param| param.to_string());
819        let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
820        let chats = get_chat_cnt(self).await?;
821        let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
822        let request_msgs = message::get_request_msg_cnt(self).await;
823        let contacts = Contact::get_real_cnt(self).await?;
824        let is_configured = self.get_config_int(Config::Configured).await?;
825        let proxy_enabled = self.get_config_int(Config::ProxyEnabled).await?;
826        let dbversion = self
827            .sql
828            .get_raw_config_int("dbversion")
829            .await?
830            .unwrap_or_default();
831        let journal_mode = self
832            .sql
833            .query_get_value("PRAGMA journal_mode;", ())
834            .await?
835            .unwrap_or_else(|| "unknown".to_string());
836        let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
837        let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
838        let bcc_self = self.get_config_int(Config::BccSelf).await?;
839        let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
840        let disable_idle = self.get_config_bool(Config::DisableIdle).await?;
841
842        let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?;
843
844        let pub_key_cnt = self
845            .sql
846            .count("SELECT COUNT(*) FROM public_keys;", ())
847            .await?;
848        let fingerprint_str = match self_fingerprint(self).await {
849            Ok(fp) => fp.to_string(),
850            Err(err) => format!("<key failure: {err}>"),
851        };
852
853        let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
854        let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
855        let only_fetch_mvbox = self.get_config_int(Config::OnlyFetchMvbox).await?;
856        let folders_configured = self
857            .sql
858            .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
859            .await?
860            .unwrap_or_default();
861
862        let configured_inbox_folder = self
863            .get_config(Config::ConfiguredInboxFolder)
864            .await?
865            .unwrap_or_else(|| "<unset>".to_string());
866        let configured_sentbox_folder = self
867            .get_config(Config::ConfiguredSentboxFolder)
868            .await?
869            .unwrap_or_else(|| "<unset>".to_string());
870        let configured_mvbox_folder = self
871            .get_config(Config::ConfiguredMvboxFolder)
872            .await?
873            .unwrap_or_else(|| "<unset>".to_string());
874        let configured_trash_folder = self
875            .get_config(Config::ConfiguredTrashFolder)
876            .await?
877            .unwrap_or_else(|| "<unset>".to_string());
878
879        let mut res = get_info();
880
881        // insert values
882        res.insert("bot", self.get_config_int(Config::Bot).await?.to_string());
883        res.insert("number_of_chats", chats.to_string());
884        res.insert("number_of_chat_messages", unblocked_msgs.to_string());
885        res.insert("messages_in_contact_requests", request_msgs.to_string());
886        res.insert("number_of_contacts", contacts.to_string());
887        res.insert("database_dir", self.get_dbfile().display().to_string());
888        res.insert("database_version", dbversion.to_string());
889        res.insert(
890            "database_encrypted",
891            self.sql
892                .is_encrypted()
893                .await
894                .map_or_else(|| "closed".to_string(), |b| b.to_string()),
895        );
896        res.insert("journal_mode", journal_mode);
897        res.insert("blobdir", self.get_blobdir().display().to_string());
898        res.insert(
899            "selfavatar",
900            self.get_config(Config::Selfavatar)
901                .await?
902                .unwrap_or_else(|| "<unset>".to_string()),
903        );
904        res.insert("is_configured", is_configured.to_string());
905        res.insert("proxy_enabled", proxy_enabled.to_string());
906        res.insert("entered_account_settings", l.to_string());
907        res.insert("used_account_settings", l2);
908
909        if let Some(server_id) = &*self.server_id.read().await {
910            res.insert("imap_server_id", format!("{server_id:?}"));
911        }
912
913        res.insert("is_chatmail", self.is_chatmail().await?.to_string());
914        res.insert(
915            "fix_is_chatmail",
916            self.get_config_bool(Config::FixIsChatmail)
917                .await?
918                .to_string(),
919        );
920        res.insert(
921            "is_muted",
922            self.get_config_bool(Config::IsMuted).await?.to_string(),
923        );
924        res.insert(
925            "private_tag",
926            self.get_config(Config::PrivateTag)
927                .await?
928                .unwrap_or_else(|| "<unset>".to_string()),
929        );
930
931        if let Some(metadata) = &*self.metadata.read().await {
932            if let Some(comment) = &metadata.comment {
933                res.insert("imap_server_comment", format!("{comment:?}"));
934            }
935
936            if let Some(admin) = &metadata.admin {
937                res.insert("imap_server_admin", format!("{admin:?}"));
938            }
939        }
940
941        res.insert("secondary_addrs", secondary_addrs);
942        res.insert(
943            "fetched_existing_msgs",
944            self.get_config_bool(Config::FetchedExistingMsgs)
945                .await?
946                .to_string(),
947        );
948        res.insert(
949            "show_emails",
950            self.get_config_int(Config::ShowEmails).await?.to_string(),
951        );
952        res.insert(
953            "download_limit",
954            self.get_config_int(Config::DownloadLimit)
955                .await?
956                .to_string(),
957        );
958        res.insert("sentbox_watch", sentbox_watch.to_string());
959        res.insert("mvbox_move", mvbox_move.to_string());
960        res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
961        res.insert(
962            constants::DC_FOLDERS_CONFIGURED_KEY,
963            folders_configured.to_string(),
964        );
965        res.insert("configured_inbox_folder", configured_inbox_folder);
966        res.insert("configured_sentbox_folder", configured_sentbox_folder);
967        res.insert("configured_mvbox_folder", configured_mvbox_folder);
968        res.insert("configured_trash_folder", configured_trash_folder);
969        res.insert("mdns_enabled", mdns_enabled.to_string());
970        res.insert("e2ee_enabled", e2ee_enabled.to_string());
971        res.insert("bcc_self", bcc_self.to_string());
972        res.insert("sync_msgs", sync_msgs.to_string());
973        res.insert("disable_idle", disable_idle.to_string());
974        res.insert("private_key_count", prv_key_cnt.to_string());
975        res.insert("public_key_count", pub_key_cnt.to_string());
976        res.insert("fingerprint", fingerprint_str);
977        res.insert(
978            "webrtc_instance",
979            self.get_config(Config::WebrtcInstance)
980                .await?
981                .unwrap_or_else(|| "<unset>".to_string()),
982        );
983        res.insert(
984            "media_quality",
985            self.get_config_int(Config::MediaQuality).await?.to_string(),
986        );
987        res.insert(
988            "delete_device_after",
989            self.get_config_int(Config::DeleteDeviceAfter)
990                .await?
991                .to_string(),
992        );
993        res.insert(
994            "delete_server_after",
995            self.get_config_int(Config::DeleteServerAfter)
996                .await?
997                .to_string(),
998        );
999        res.insert(
1000            "delete_to_trash",
1001            self.get_config(Config::DeleteToTrash)
1002                .await?
1003                .unwrap_or_else(|| "<unset>".to_string()),
1004        );
1005        res.insert(
1006            "last_housekeeping",
1007            self.get_config_int(Config::LastHousekeeping)
1008                .await?
1009                .to_string(),
1010        );
1011        res.insert(
1012            "last_cant_decrypt_outgoing_msgs",
1013            self.get_config_int(Config::LastCantDecryptOutgoingMsgs)
1014                .await?
1015                .to_string(),
1016        );
1017        res.insert(
1018            "scan_all_folders_debounce_secs",
1019            self.get_config_int(Config::ScanAllFoldersDebounceSecs)
1020                .await?
1021                .to_string(),
1022        );
1023        res.insert(
1024            "quota_exceeding",
1025            self.get_config_int(Config::QuotaExceeding)
1026                .await?
1027                .to_string(),
1028        );
1029        res.insert(
1030            "authserv_id_candidates",
1031            self.get_config(Config::AuthservIdCandidates)
1032                .await?
1033                .unwrap_or_default(),
1034        );
1035        res.insert(
1036            "sign_unencrypted",
1037            self.get_config_int(Config::SignUnencrypted)
1038                .await?
1039                .to_string(),
1040        );
1041        res.insert(
1042            "protect_autocrypt",
1043            self.get_config_int(Config::ProtectAutocrypt)
1044                .await?
1045                .to_string(),
1046        );
1047        res.insert(
1048            "debug_logging",
1049            self.get_config_int(Config::DebugLogging).await?.to_string(),
1050        );
1051        res.insert(
1052            "last_msg_id",
1053            self.get_config_int(Config::LastMsgId).await?.to_string(),
1054        );
1055        res.insert(
1056            "gossip_period",
1057            self.get_config_int(Config::GossipPeriod).await?.to_string(),
1058        );
1059        res.insert(
1060            "verified_one_on_one_chats", // deprecated 2025-07
1061            self.get_config_bool(Config::VerifiedOneOnOneChats)
1062                .await?
1063                .to_string(),
1064        );
1065        res.insert(
1066            "webxdc_realtime_enabled",
1067            self.get_config_bool(Config::WebxdcRealtimeEnabled)
1068                .await?
1069                .to_string(),
1070        );
1071        res.insert(
1072            "donation_request_next_check",
1073            self.get_config_i64(Config::DonationRequestNextCheck)
1074                .await?
1075                .to_string(),
1076        );
1077        res.insert(
1078            "first_key_contacts_msg_id",
1079            self.sql
1080                .get_raw_config("first_key_contacts_msg_id")
1081                .await?
1082                .unwrap_or_default(),
1083        );
1084
1085        let elapsed = time_elapsed(&self.creation_time);
1086        res.insert("uptime", duration_to_str(elapsed));
1087
1088        Ok(res)
1089    }
1090
1091    async fn get_self_report(&self) -> Result<String> {
1092        #[derive(Default)]
1093        struct ChatNumbers {
1094            protected: u32,
1095            opportunistic_dc: u32,
1096            opportunistic_mua: u32,
1097            unencrypted_dc: u32,
1098            unencrypted_mua: u32,
1099        }
1100
1101        let mut res = String::new();
1102        res += &format!("core_version {}\n", get_version_str());
1103
1104        let num_msgs: u32 = self
1105            .sql
1106            .query_get_value(
1107                "SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id!=?",
1108                (DC_CHAT_ID_TRASH,),
1109            )
1110            .await?
1111            .unwrap_or_default();
1112        res += &format!("num_msgs {num_msgs}\n");
1113
1114        let num_chats: u32 = self
1115            .sql
1116            .query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
1117            .await?
1118            .unwrap_or_default();
1119        res += &format!("num_chats {num_chats}\n");
1120
1121        let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
1122        res += &format!("db_size_bytes {db_size}\n");
1123
1124        let secret_key = &load_self_secret_key(self).await?.primary_key;
1125        let key_created = secret_key.public_key().created_at().timestamp();
1126        res += &format!("key_created {key_created}\n");
1127
1128        // how many of the chats active in the last months are:
1129        // - protected
1130        // - opportunistic-encrypted and the contact uses Delta Chat
1131        // - opportunistic-encrypted and the contact uses a classical MUA
1132        // - unencrypted and the contact uses Delta Chat
1133        // - unencrypted and the contact uses a classical MUA
1134        let three_months_ago = time().saturating_sub(3600 * 24 * 30 * 3);
1135        let chats = self
1136            .sql
1137            .query_map(
1138                "SELECT c.protected, m.param, m.msgrmsg
1139                    FROM chats c
1140                    JOIN msgs m
1141                        ON c.id=m.chat_id
1142                        AND m.id=(
1143                                SELECT id
1144                                FROM msgs
1145                                WHERE chat_id=c.id
1146                                AND hidden=0
1147                                AND download_state=?
1148                                AND to_id!=?
1149                                ORDER BY timestamp DESC, id DESC LIMIT 1)
1150                    WHERE c.id>9
1151                    AND (c.blocked=0 OR c.blocked=2)
1152                    AND IFNULL(m.timestamp,c.created_timestamp) > ?
1153                    GROUP BY c.id",
1154                (DownloadState::Done, ContactId::INFO, three_months_ago),
1155                |row| {
1156                    let protected: ProtectionStatus = row.get(0)?;
1157                    let message_param: Params =
1158                        row.get::<_, String>(1)?.parse().unwrap_or_default();
1159                    let is_dc_message: bool = row.get(2)?;
1160                    Ok((protected, message_param, is_dc_message))
1161                },
1162                |rows| {
1163                    let mut chats = ChatNumbers::default();
1164                    for row in rows {
1165                        let (protected, message_param, is_dc_message) = row?;
1166                        let encrypted = message_param
1167                            .get_bool(Param::GuaranteeE2ee)
1168                            .unwrap_or(false);
1169
1170                        if protected == ProtectionStatus::Protected {
1171                            chats.protected += 1;
1172                        } else if encrypted {
1173                            if is_dc_message {
1174                                chats.opportunistic_dc += 1;
1175                            } else {
1176                                chats.opportunistic_mua += 1;
1177                            }
1178                        } else if is_dc_message {
1179                            chats.unencrypted_dc += 1;
1180                        } else {
1181                            chats.unencrypted_mua += 1;
1182                        }
1183                    }
1184                    Ok(chats)
1185                },
1186            )
1187            .await?;
1188        res += &format!("chats_protected {}\n", chats.protected);
1189        res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
1190        res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
1191        res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
1192        res += &format!("chats_unencrypted_mua {}\n", chats.unencrypted_mua);
1193
1194        let self_reporting_id = match self.get_config(Config::SelfReportingId).await? {
1195            Some(id) => id,
1196            None => {
1197                let id = create_id();
1198                self.set_config(Config::SelfReportingId, Some(&id)).await?;
1199                id
1200            }
1201        };
1202        res += &format!("self_reporting_id {self_reporting_id}");
1203
1204        Ok(res)
1205    }
1206
1207    /// Drafts a message with statistics about the usage of Delta Chat.
1208    /// The user can inspect the message if they want, and then hit "Send".
1209    ///
1210    /// On the other end, a bot will receive the message and make it available
1211    /// to Delta Chat's developers.
1212    pub async fn draft_self_report(&self) -> Result<ChatId> {
1213        const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
1214        let contact_id: ContactId = *import_vcard(self, SELF_REPORTING_BOT_VCARD)
1215            .await?
1216            .first()
1217            .context("Self reporting bot vCard does not contain a contact")?;
1218        mark_contact_id_as_verified(self, contact_id, ContactId::SELF).await?;
1219
1220        let chat_id = ChatId::create_for_contact(self, contact_id).await?;
1221        chat_id
1222            .set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
1223            .await?;
1224
1225        let mut msg = Message::new_text(self.get_self_report().await?);
1226
1227        chat_id.set_draft(self, Some(&mut msg)).await?;
1228
1229        Ok(chat_id)
1230    }
1231
1232    /// Get a list of fresh, unmuted messages in unblocked chats.
1233    ///
1234    /// The list starts with the most recent message
1235    /// and is typically used to show notifications.
1236    /// Moreover, the number of returned messages
1237    /// can be used for a badge counter on the app icon.
1238    pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
1239        let list = self
1240            .sql
1241            .query_map(
1242                concat!(
1243                    "SELECT m.id",
1244                    " FROM msgs m",
1245                    " LEFT JOIN contacts ct",
1246                    "        ON m.from_id=ct.id",
1247                    " LEFT JOIN chats c",
1248                    "        ON m.chat_id=c.id",
1249                    " WHERE m.state=?",
1250                    "   AND m.hidden=0",
1251                    "   AND m.chat_id>9",
1252                    "   AND ct.blocked=0",
1253                    "   AND c.blocked=0",
1254                    "   AND NOT(c.muted_until=-1 OR c.muted_until>?)",
1255                    " ORDER BY m.timestamp DESC,m.id DESC;"
1256                ),
1257                (MessageState::InFresh, time()),
1258                |row| row.get::<_, MsgId>(0),
1259                |rows| {
1260                    let mut list = Vec::new();
1261                    for row in rows {
1262                        list.push(row?);
1263                    }
1264                    Ok(list)
1265                },
1266            )
1267            .await?;
1268        Ok(list)
1269    }
1270
1271    /// Returns a list of messages with database ID higher than requested.
1272    ///
1273    /// Blocked contacts and chats are excluded,
1274    /// but self-sent messages and contact requests are included in the results.
1275    pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
1276        let last_msg_id = match self.get_config(Config::LastMsgId).await? {
1277            Some(s) => MsgId::new(s.parse()?),
1278            None => {
1279                // If `last_msg_id` is not set yet,
1280                // subtract 1 from the last id,
1281                // so a single message is returned and can
1282                // be marked as seen.
1283                self.sql
1284                    .query_row(
1285                        "SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)",
1286                        (),
1287                        |row| {
1288                            let msg_id: MsgId = row.get(0)?;
1289                            Ok(msg_id)
1290                        },
1291                    )
1292                    .await?
1293            }
1294        };
1295
1296        let list = self
1297            .sql
1298            .query_map(
1299                "SELECT m.id
1300                     FROM msgs m
1301                     LEFT JOIN contacts ct
1302                            ON m.from_id=ct.id
1303                     LEFT JOIN chats c
1304                            ON m.chat_id=c.id
1305                     WHERE m.id>?
1306                       AND m.hidden=0
1307                       AND m.chat_id>9
1308                       AND ct.blocked=0
1309                       AND c.blocked!=1
1310                     ORDER BY m.id ASC",
1311                (
1312                    last_msg_id.to_u32(), // Explicitly convert to u32 because 0 is allowed.
1313                ),
1314                |row| {
1315                    let msg_id: MsgId = row.get(0)?;
1316                    Ok(msg_id)
1317                },
1318                |rows| {
1319                    let mut list = Vec::new();
1320                    for row in rows {
1321                        list.push(row?);
1322                    }
1323                    Ok(list)
1324                },
1325            )
1326            .await?;
1327        Ok(list)
1328    }
1329
1330    /// Returns a list of messages with database ID higher than last marked as seen.
1331    ///
1332    /// This function is supposed to be used by bot to request messages
1333    /// that are not processed yet.
1334    ///
1335    /// Waits for notification and returns a result.
1336    /// Note that the result may be empty if the message is deleted
1337    /// shortly after notification or notification is manually triggered
1338    /// to interrupt waiting.
1339    /// Notification may be manually triggered by calling [`Self::stop_io`].
1340    pub async fn wait_next_msgs(&self) -> Result<Vec<MsgId>> {
1341        self.new_msgs_notify.notified().await;
1342        let list = self.get_next_msgs().await?;
1343        Ok(list)
1344    }
1345
1346    /// Searches for messages containing the query string case-insensitively.
1347    ///
1348    /// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
1349    /// is `None` this searches messages from all chats.
1350    ///
1351    /// NB: Wrt the search in long messages which are shown truncated with the "Show Full Message…"
1352    /// button, we only look at the first several kilobytes. Let's not fix this -- one can send a
1353    /// dictionary in the message that matches any reasonable search request, but the user won't see
1354    /// the match because they should tap on "Show Full Message…" for that. Probably such messages
1355    /// would only clutter search results.
1356    pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: &str) -> Result<Vec<MsgId>> {
1357        let real_query = query.trim().to_lowercase();
1358        if real_query.is_empty() {
1359            return Ok(Vec::new());
1360        }
1361        let str_like_in_text = format!("%{real_query}%");
1362
1363        let list = if let Some(chat_id) = chat_id {
1364            self.sql
1365                .query_map(
1366                    "SELECT m.id AS id
1367                 FROM msgs m
1368                 LEFT JOIN contacts ct
1369                        ON m.from_id=ct.id
1370                 WHERE m.chat_id=?
1371                   AND m.hidden=0
1372                   AND ct.blocked=0
1373                   AND IFNULL(txt_normalized, txt) LIKE ?
1374                 ORDER BY m.timestamp,m.id;",
1375                    (chat_id, str_like_in_text),
1376                    |row| row.get::<_, MsgId>("id"),
1377                    |rows| {
1378                        let mut ret = Vec::new();
1379                        for id in rows {
1380                            ret.push(id?);
1381                        }
1382                        Ok(ret)
1383                    },
1384                )
1385                .await?
1386        } else {
1387            // For performance reasons results are sorted only by `id`, that is in the order of
1388            // message reception.
1389            //
1390            // Unlike chat view, sorting by `timestamp` is not necessary but slows down the query by
1391            // ~25% according to benchmarks.
1392            //
1393            // To speed up incremental search, where queries for few characters usually return lots
1394            // of unwanted results that are discarded moments later, we added `LIMIT 1000`.
1395            // According to some tests, this limit speeds up eg. 2 character searches by factor 10.
1396            // The limit is documented and UI may add a hint when getting 1000 results.
1397            self.sql
1398                .query_map(
1399                    "SELECT m.id AS id
1400                 FROM msgs m
1401                 LEFT JOIN contacts ct
1402                        ON m.from_id=ct.id
1403                 LEFT JOIN chats c
1404                        ON m.chat_id=c.id
1405                 WHERE m.chat_id>9
1406                   AND m.hidden=0
1407                   AND c.blocked!=1
1408                   AND ct.blocked=0
1409                   AND IFNULL(txt_normalized, txt) LIKE ?
1410                 ORDER BY m.id DESC LIMIT 1000",
1411                    (str_like_in_text,),
1412                    |row| row.get::<_, MsgId>("id"),
1413                    |rows| {
1414                        let mut ret = Vec::new();
1415                        for id in rows {
1416                            ret.push(id?);
1417                        }
1418                        Ok(ret)
1419                    },
1420                )
1421                .await?
1422        };
1423
1424        Ok(list)
1425    }
1426
1427    /// Returns true if given folder name is the name of the inbox.
1428    pub async fn is_inbox(&self, folder_name: &str) -> Result<bool> {
1429        let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
1430        Ok(inbox.as_deref() == Some(folder_name))
1431    }
1432
1433    /// Returns true if given folder name is the name of the "sent" folder.
1434    pub async fn is_sentbox(&self, folder_name: &str) -> Result<bool> {
1435        let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
1436        Ok(sentbox.as_deref() == Some(folder_name))
1437    }
1438
1439    /// Returns true if given folder name is the name of the "DeltaChat" folder.
1440    pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
1441        let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
1442        Ok(mvbox.as_deref() == Some(folder_name))
1443    }
1444
1445    /// Returns true if given folder name is the name of the trash folder.
1446    pub async fn is_trash(&self, folder_name: &str) -> Result<bool> {
1447        let trash = self.get_config(Config::ConfiguredTrashFolder).await?;
1448        Ok(trash.as_deref() == Some(folder_name))
1449    }
1450
1451    pub(crate) async fn should_delete_to_trash(&self) -> Result<bool> {
1452        if let Some(v) = self.get_config_bool_opt(Config::DeleteToTrash).await? {
1453            return Ok(v);
1454        }
1455        if let Some(provider) = self.get_configured_provider().await? {
1456            return Ok(provider.opt.delete_to_trash);
1457        }
1458        Ok(false)
1459    }
1460
1461    /// Returns `target` for deleted messages as per `imap` table. Empty string means "delete w/o
1462    /// moving to trash".
1463    pub(crate) async fn get_delete_msgs_target(&self) -> Result<String> {
1464        if !self.should_delete_to_trash().await? {
1465            return Ok("".into());
1466        }
1467        self.get_config(Config::ConfiguredTrashFolder)
1468            .await?
1469            .context("No configured trash folder")
1470    }
1471
1472    pub(crate) fn derive_blobdir(dbfile: &Path) -> PathBuf {
1473        let mut blob_fname = OsString::new();
1474        blob_fname.push(dbfile.file_name().unwrap_or_default());
1475        blob_fname.push("-blobs");
1476        dbfile.with_file_name(blob_fname)
1477    }
1478
1479    pub(crate) fn derive_walfile(dbfile: &Path) -> PathBuf {
1480        let mut wal_fname = OsString::new();
1481        wal_fname.push(dbfile.file_name().unwrap_or_default());
1482        wal_fname.push("-wal");
1483        dbfile.with_file_name(wal_fname)
1484    }
1485}
1486
1487/// Returns core version as a string.
1488pub fn get_version_str() -> &'static str {
1489    &DC_VERSION_STR
1490}
1491
1492#[cfg(test)]
1493mod context_tests;