Skip to main content

deltachat/
accounts.rs

1//! # Account manager module.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::future::Future;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use anyhow::{Context as _, Result, bail, ensure};
9use async_channel::{self, Receiver, Sender};
10use futures::FutureExt as _;
11use futures::future;
12use futures_lite::FutureExt as _;
13use serde::{Deserialize, Serialize};
14use tokio::fs;
15use tokio::io::AsyncWriteExt;
16use tokio::task::{JoinHandle, JoinSet};
17use uuid::Uuid;
18
19#[cfg(not(target_os = "ios"))]
20use tokio::sync::oneshot;
21#[cfg(not(target_os = "ios"))]
22use tokio::time::{Duration, sleep};
23
24use crate::context::{Context, ContextBuilder};
25use crate::events::{Event, EventEmitter, EventType, Events};
26use crate::location;
27use crate::log::warn;
28use crate::push::PushSubscriber;
29use crate::stock_str::StockStrings;
30
31/// Account manager, that can handle multiple accounts in a single place.
32#[derive(Debug)]
33pub struct Accounts {
34    dir: PathBuf,
35    config: Config,
36    /// Map from account ID to the account.
37    accounts: BTreeMap<u32, Context>,
38
39    /// Event channel to emit account manager errors.
40    events: Events,
41
42    /// Stock string translations shared by all created contexts.
43    ///
44    /// This way changing a translation for one context automatically
45    /// changes it for all other contexts.
46    pub(crate) stockstrings: StockStrings,
47
48    /// Push notification subscriber shared between accounts.
49    push_subscriber: PushSubscriber,
50
51    /// Channel sender to cancel ongoing background_fetch().
52    ///
53    /// If background_fetch() is not running, this is `None`.
54    /// New background_fetch() should not be started if this
55    /// contains `Some`.
56    background_fetch_interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
57}
58
59impl Accounts {
60    /// Loads or creates an accounts folder at the given `dir`.
61    pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
62        if writable {
63            Self::ensure_accounts_dir(&dir).await?;
64        }
65        let events = Events::new();
66        Accounts::open(events, dir, writable).await
67    }
68
69    /// Loads or creates an accounts folder at the given `dir`.
70    /// Uses an existing events channel.
71    pub async fn new_with_events(dir: PathBuf, writable: bool, events: Events) -> Result<Self> {
72        if writable {
73            Self::ensure_accounts_dir(&dir).await?;
74        }
75        Accounts::open(events, dir, writable).await
76    }
77
78    /// Get the ID used to log events.
79    ///
80    /// Account manager logs events with ID 0
81    /// which is not used by any accounts.
82    fn get_id(&self) -> u32 {
83        0
84    }
85
86    /// Ensures the accounts directory and config file exist.
87    /// Creates them if the directory doesn't exist, or if it exists but is empty.
88    /// Errors if the directory exists with files but no config.
89    async fn ensure_accounts_dir(dir: &Path) -> Result<()> {
90        if !dir.exists() {
91            fs::create_dir_all(dir)
92                .await
93                .context("Failed to create folder")?;
94            Config::new(dir).await?;
95        } else if !dir.join(CONFIG_NAME).exists() {
96            let mut rd = fs::read_dir(dir).await?;
97            ensure!(rd.next_entry().await?.is_none(), "{dir:?} is not empty");
98            Config::new(dir).await?;
99        }
100        Ok(())
101    }
102
103    /// Opens an existing accounts structure. Will error if the folder doesn't exist,
104    /// no account exists and no config exists.
105    async fn open(events: Events, dir: PathBuf, writable: bool) -> Result<Self> {
106        ensure!(dir.exists(), "directory does not exist");
107
108        let config_file = dir.join(CONFIG_NAME);
109        ensure!(config_file.exists(), "{config_file:?} does not exist");
110
111        let config = Config::from_file(config_file, writable).await?;
112
113        let stockstrings = StockStrings::new();
114        let push_subscriber = PushSubscriber::new();
115        let accounts = config
116            .load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
117            .await
118            .context("failed to load accounts")?;
119
120        Ok(Self {
121            dir,
122            config,
123            accounts,
124            events,
125            stockstrings,
126            push_subscriber,
127            background_fetch_interrupt_sender: Default::default(),
128        })
129    }
130
131    /// Returns an account by its `id`:
132    pub fn get_account(&self, id: u32) -> Option<Context> {
133        self.accounts.get(&id).cloned()
134    }
135
136    /// Returns the currently selected account.
137    pub fn get_selected_account(&self) -> Option<Context> {
138        let id = self.config.get_selected_account();
139        self.accounts.get(&id).cloned()
140    }
141
142    /// Returns the currently selected account's id or None if no account is selected.
143    pub fn get_selected_account_id(&self) -> Option<u32> {
144        match self.config.get_selected_account() {
145            0 => None,
146            id => Some(id),
147        }
148    }
149
150    /// Selects the given account.
151    pub async fn select_account(&mut self, id: u32) -> Result<()> {
152        self.config.select_account(id).await?;
153
154        Ok(())
155    }
156
157    /// Adds a new account and opens it.
158    ///
159    /// Returns account ID.
160    pub async fn add_account(&mut self) -> Result<u32> {
161        let account_config = self.config.new_account().await?;
162        let dbfile = account_config.dbfile(&self.dir);
163
164        let ctx = ContextBuilder::new(dbfile)
165            .with_id(account_config.id)
166            .with_events(self.events.clone())
167            .with_stock_strings(self.stockstrings.clone())
168            .with_push_subscriber(self.push_subscriber.clone())
169            .build()
170            .await?;
171        // Try to open without a passphrase,
172        // but do not return an error if account is passphare-protected.
173        ctx.open("".to_string()).await?;
174
175        self.accounts.insert(account_config.id, ctx);
176        self.emit_event(EventType::AccountsChanged);
177
178        Ok(account_config.id)
179    }
180
181    /// Adds a new closed account.
182    pub async fn add_closed_account(&mut self) -> Result<u32> {
183        let account_config = self.config.new_account().await?;
184        let dbfile = account_config.dbfile(&self.dir);
185
186        let ctx = ContextBuilder::new(dbfile)
187            .with_id(account_config.id)
188            .with_events(self.events.clone())
189            .with_stock_strings(self.stockstrings.clone())
190            .with_push_subscriber(self.push_subscriber.clone())
191            .build()
192            .await?;
193        self.accounts.insert(account_config.id, ctx);
194        self.emit_event(EventType::AccountsChanged);
195
196        Ok(account_config.id)
197    }
198
199    /// Removes an account.
200    pub async fn remove_account(&mut self, id: u32) -> Result<()> {
201        let ctx = self
202            .accounts
203            .remove(&id)
204            .with_context(|| format!("no account with id {id}"))?;
205        ctx.stop_io().await;
206
207        // Explicitly close the database
208        // to make sure the database file is closed
209        // and can be removed on Windows.
210        // If some spawned task tries to use the database afterwards,
211        // it will fail.
212        //
213        // Previously `stop_io()` aborted the tasks without awaiting them
214        // and this resulted in keeping `Context` clones inside
215        // `Future`s that were not dropped. This bug is fixed now,
216        // but explicitly closing the database ensures that file is freed
217        // even if not all `Context` references are dropped.
218        ctx.sql.close().await;
219        drop(ctx);
220
221        if let Some(cfg) = self.config.get_account(id) {
222            let account_path = self.dir.join(cfg.dir);
223
224            try_many_times(|| fs::remove_dir_all(&account_path))
225                .await
226                .context("failed to remove account data")?;
227        }
228        self.config.remove_account(id).await?;
229        self.emit_event(EventType::AccountsChanged);
230
231        Ok(())
232    }
233
234    /// Migrates an existing account into this structure.
235    ///
236    /// Returns the ID of new account.
237    pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
238        let blobdir = Context::derive_blobdir(&dbfile);
239        let walfile = Context::derive_walfile(&dbfile);
240
241        ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
242        ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
243
244        let old_id = self.config.get_selected_account();
245
246        // create new account
247        let account_config = self
248            .config
249            .new_account()
250            .await
251            .context("failed to create new account")?;
252
253        let new_dbfile = account_config.dbfile(&self.dir);
254        let new_blobdir = Context::derive_blobdir(&new_dbfile);
255        let new_walfile = Context::derive_walfile(&new_dbfile);
256
257        let res = {
258            fs::create_dir_all(self.dir.join(&account_config.dir))
259                .await
260                .context("failed to create dir")?;
261            try_many_times(|| fs::rename(&dbfile, &new_dbfile))
262                .await
263                .context("failed to rename dbfile")?;
264            try_many_times(|| fs::rename(&blobdir, &new_blobdir))
265                .await
266                .context("failed to rename blobdir")?;
267            if walfile.exists() {
268                fs::rename(&walfile, &new_walfile)
269                    .await
270                    .context("failed to rename walfile")?;
271            }
272            Ok(())
273        };
274
275        match res {
276            Ok(_) => {
277                let ctx = Context::new(
278                    &new_dbfile,
279                    account_config.id,
280                    self.events.clone(),
281                    self.stockstrings.clone(),
282                )
283                .await?;
284                self.accounts.insert(account_config.id, ctx);
285                Ok(account_config.id)
286            }
287            Err(err) => {
288                let account_path = std::path::PathBuf::from(&account_config.dir);
289                try_many_times(|| fs::remove_dir_all(&account_path))
290                    .await
291                    .context("failed to remove account data")?;
292                self.config.remove_account(account_config.id).await?;
293
294                // set selection back
295                self.select_account(old_id).await?;
296
297                Err(err)
298            }
299        }
300    }
301
302    /// Gets a list of all account ids in the user-configured order.
303    pub fn get_all(&self) -> Vec<u32> {
304        let mut ordered_ids = Vec::new();
305        let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
306
307        // First, add accounts in the configured order
308        for &id in &self.config.inner.accounts_order {
309            if all_ids.remove(&id) {
310                ordered_ids.push(id);
311            }
312        }
313
314        // Then add any accounts not in the order list (newly added accounts)
315        for id in all_ids {
316            ordered_ids.push(id);
317        }
318
319        ordered_ids
320    }
321
322    /// Sets the order of accounts.
323    ///
324    /// The provided list should contain all account IDs in the desired order.
325    /// If an account ID is missing from the list, it will be appended at the end.
326    /// If the list contains non-existent account IDs, they will be ignored.
327    pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
328        let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
329
330        // Filter out non-existent account IDs
331        let mut filtered_order: Vec<u32> = order
332            .into_iter()
333            .filter(|id| existing_ids.contains(id))
334            .collect();
335
336        // Add any missing account IDs at the end
337        for &id in &existing_ids {
338            if !filtered_order.contains(&id) {
339                filtered_order.push(id);
340            }
341        }
342
343        self.config.inner.accounts_order = filtered_order;
344        self.config.sync().await?;
345        self.emit_event(EventType::AccountsChanged);
346        Ok(())
347    }
348
349    /// Starts background tasks such as IMAP and SMTP loops for all accounts.
350    pub async fn start_io(&mut self) {
351        for account in self.accounts.values_mut() {
352            account.start_io().await;
353        }
354    }
355
356    /// Stops background tasks for all accounts.
357    pub async fn stop_io(&self) {
358        // Sending an event here wakes up event loop even
359        // if there are no accounts.
360        info!(self, "Stopping IO for all accounts.");
361        for account in self.accounts.values() {
362            account.stop_io().await;
363        }
364    }
365
366    /// Notifies all accounts that the network may have become available.
367    pub async fn maybe_network(&self) {
368        for account in self.accounts.values() {
369            account.scheduler.maybe_network().await;
370        }
371    }
372
373    /// Notifies all accounts that the network connection may have been lost.
374    pub async fn maybe_network_lost(&self) {
375        for account in self.accounts.values() {
376            account.scheduler.maybe_network_lost(account).await;
377        }
378    }
379
380    /// Performs a background fetch for all accounts in parallel.
381    ///
382    /// This is an auxiliary function and not part of public API.
383    /// Use [Accounts::background_fetch] instead.
384    ///
385    /// This function is cancellation-safe.
386    /// It is intended to be cancellable,
387    /// either because of the timeout or because background
388    /// fetch was explicitly cancelled.
389    async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
390        let n_accounts = accounts.len();
391        events.emit(Event {
392            id: 0,
393            typ: EventType::Info(format!(
394                "Starting background fetch for {n_accounts} accounts."
395            )),
396        });
397        ::tracing::event!(
398            ::tracing::Level::INFO,
399            account_id = 0,
400            "Starting background fetch for {n_accounts} accounts."
401        );
402        let mut set = JoinSet::new();
403        for account in accounts {
404            set.spawn(async move {
405                if let Err(error) = account.background_fetch().await {
406                    warn!(account, "{error:#}");
407                }
408            });
409        }
410        set.join_all().await;
411        events.emit(Event {
412            id: 0,
413            typ: EventType::Info(format!(
414                "Finished background fetch for {n_accounts} accounts."
415            )),
416        });
417        ::tracing::event!(
418            ::tracing::Level::INFO,
419            account_id = 0,
420            "Finished background fetch for {n_accounts} accounts."
421        );
422    }
423
424    /// Auxiliary function for [Accounts::background_fetch].
425    ///
426    /// Runs `background_fetch` until it finishes
427    /// or until the timeout.
428    ///
429    /// Produces `AccountsBackgroundFetchDone` event in every case
430    /// and clears [`Self::background_fetch_interrupt_sender`]
431    /// so a new background fetch can be started.
432    ///
433    /// This function is not cancellation-safe.
434    /// Cancelling it before it returns may result
435    /// in not being able to run any new background fetch
436    /// if interrupt sender was not cleared.
437    async fn background_fetch_with_timeout(
438        accounts: Vec<Context>,
439        events: Events,
440        timeout: std::time::Duration,
441        interrupt_sender: Arc<parking_lot::Mutex<Option<Sender<()>>>>,
442        interrupt_receiver: Option<Receiver<()>>,
443    ) {
444        let Some(interrupt_receiver) = interrupt_receiver else {
445            // Nothing to do if we got no interrupt receiver.
446            return;
447        };
448        if let Err(_err) = tokio::time::timeout(
449            timeout,
450            Self::background_fetch_no_timeout(accounts, events.clone())
451                .race(interrupt_receiver.recv().map(|_| ())),
452        )
453        .await
454        {
455            events.emit(Event {
456                id: 0,
457                typ: EventType::Warning("Background fetch timed out.".to_string()),
458            });
459            ::tracing::event!(
460                ::tracing::Level::WARN,
461                account_id = 0,
462                "Background fetch timed out."
463            );
464        }
465        events.emit(Event {
466            id: 0,
467            typ: EventType::AccountsBackgroundFetchDone,
468        });
469        (*interrupt_sender.lock()) = None;
470    }
471
472    /// Performs a background fetch for all accounts in parallel with a timeout.
473    ///
474    /// Ongoing background fetch can also be cancelled manually
475    /// by calling `stop_background_fetch()`, in which case it will
476    /// return immediately even before the timeout expiration
477    /// or finishing fetching.
478    ///
479    /// The `AccountsBackgroundFetchDone` event is emitted at the end,
480    /// process all events until you get this one and you can safely return to the background
481    /// without forgetting to create notifications caused by timing race conditions.
482    ///
483    /// Returns a future that resolves when background fetch is done,
484    /// but does not capture `&self`.
485    pub fn background_fetch(
486        &self,
487        timeout: std::time::Duration,
488    ) -> impl Future<Output = ()> + use<> {
489        let accounts: Vec<Context> = self.accounts.values().cloned().collect();
490        let events = self.events.clone();
491        let (sender, receiver) = async_channel::bounded(1);
492        let receiver = {
493            let mut lock = self.background_fetch_interrupt_sender.lock();
494            if (*lock).is_some() {
495                // Another background_fetch() is already running,
496                // return immeidately.
497                None
498            } else {
499                *lock = Some(sender);
500                Some(receiver)
501            }
502        };
503        Self::background_fetch_with_timeout(
504            accounts,
505            events,
506            timeout,
507            self.background_fetch_interrupt_sender.clone(),
508            receiver,
509        )
510    }
511
512    /// Interrupts ongoing background_fetch() call,
513    /// making it return early.
514    ///
515    /// This method allows to cancel background_fetch() early,
516    /// e.g. on Android, when `Service.onTimeout` is called.
517    ///
518    /// If there is no ongoing background_fetch(), does nothing.
519    pub fn stop_background_fetch(&self) {
520        let mut lock = self.background_fetch_interrupt_sender.lock();
521        if let Some(sender) = lock.take() {
522            sender.try_send(()).ok();
523        }
524    }
525
526    /// Emits a single event.
527    pub fn emit_event(&self, event: EventType) {
528        self.events.emit(Event { id: 0, typ: event })
529    }
530
531    /// Returns event emitter.
532    pub fn get_event_emitter(&self) -> EventEmitter {
533        self.events.get_emitter()
534    }
535
536    /// Sets notification token for Apple Push Notification service.
537    pub async fn set_push_device_token(&self, token: &str) -> Result<()> {
538        self.push_subscriber.set_device_token(token).await;
539        Ok(())
540    }
541
542    /// Sets location for all accounts.
543    ///
544    /// Returns true if location should still be streamed.
545    pub async fn set_location(&self, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
546        let continue_streaming = future::try_join_all(self.accounts.iter().map(
547            |(account_id, account)| async move {
548                location::set(account, latitude, longitude, accuracy)
549                    .await
550                    .with_context(|| format!("Failed to set location for account {account_id}"))
551            },
552        ))
553        .await?
554        .into_iter()
555        .any(|continue_streaming| continue_streaming);
556        Ok(continue_streaming)
557    }
558
559    /// Stops sending locations to all chats.
560    pub async fn stop_sending_locations(&self) -> Result<()> {
561        future::try_join_all(
562            self.accounts
563                .iter()
564                .map(|(account_id, account)| async move {
565                    location::stop_sending(account).await.with_context(|| {
566                        format!("Failed to stop sending locations for account {account_id}")
567                    })
568                }),
569        )
570        .await?;
571        Ok(())
572    }
573}
574
575/// Configuration file name.
576const CONFIG_NAME: &str = "accounts.toml";
577
578/// Lockfile name.
579#[cfg(not(target_os = "ios"))]
580const LOCKFILE_NAME: &str = "accounts.lock";
581
582/// Database file name.
583const DB_NAME: &str = "dc.db";
584
585/// Account manager configuration file.
586#[derive(Debug)]
587struct Config {
588    file: PathBuf,
589    inner: InnerConfig,
590    // We lock the lockfile in the Config constructors to protect also from having multiple Config
591    // objects for the same config file.
592    lock_task: Option<JoinHandle<anyhow::Result<()>>>,
593}
594
595/// Account manager configuration file contents.
596///
597/// This is serialized into TOML.
598#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
599struct InnerConfig {
600    /// The currently selected account.
601    pub selected_account: u32,
602    pub next_id: u32,
603    pub accounts: Vec<AccountConfig>,
604    /// Ordered list of account IDs, representing the user's preferred order.
605    /// If an account ID is not in this list, it will be appended at the end.
606    #[serde(default)]
607    pub accounts_order: Vec<u32>,
608}
609
610impl Drop for Config {
611    fn drop(&mut self) {
612        if let Some(lock_task) = self.lock_task.take() {
613            lock_task.abort();
614        }
615    }
616}
617
618impl Config {
619    #[cfg(target_os = "ios")]
620    async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
621        // Do not lock accounts.toml on iOS.
622        // This results in 0xdead10cc crashes on suspend.
623        // iOS itself ensures that multiple instances of Delta Chat are not running.
624        Ok(None)
625    }
626
627    #[cfg(not(target_os = "ios"))]
628    #[expect(clippy::arithmetic_side_effects)]
629    async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
630        let lockfile = dir.join(LOCKFILE_NAME);
631        let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
632        let (locked_tx, locked_rx) = oneshot::channel();
633        let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
634            let mut timeout = Duration::from_millis(100);
635            let _guard = loop {
636                match lock.try_write() {
637                    Ok(guard) => break Ok(guard),
638                    Err(err) => {
639                        if timeout.as_millis() > 1600 {
640                            break Err(err);
641                        }
642                        // We need to wait for the previous lock_task to be aborted thus unlocking
643                        // the lockfile. We don't open configs for writing often outside of the
644                        // tests, so this adds delays to the tests, but otherwise ok.
645                        sleep(timeout).await;
646                        if err.kind() == std::io::ErrorKind::WouldBlock {
647                            timeout *= 2;
648                        }
649                    }
650                }
651            }?;
652            locked_tx
653                .send(())
654                .ok()
655                .context("Cannot notify about lockfile locking")?;
656            let (_tx, rx) = oneshot::channel();
657            rx.await?;
658            Ok(())
659        });
660        if locked_rx.await.is_err() {
661            bail!(
662                "Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)"
663            );
664        };
665        Ok(Some(lock_task))
666    }
667
668    /// Creates a new Config for `file`, but doesn't open/sync it.
669    async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
670        let dir = file.parent().context("Cannot get config file directory")?;
671        let inner = InnerConfig {
672            accounts: Vec::new(),
673            selected_account: 0,
674            next_id: 1,
675            accounts_order: Vec::new(),
676        };
677        if !lock {
678            let cfg = Self {
679                file,
680                inner,
681                lock_task: None,
682            };
683            return Ok(cfg);
684        }
685        let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
686        let cfg = Self {
687            file,
688            inner,
689            lock_task,
690        };
691        Ok(cfg)
692    }
693
694    /// Creates a new configuration file in the given account manager directory.
695    pub async fn new(dir: &Path) -> Result<Self> {
696        let lock = true;
697        let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
698        cfg.sync().await?;
699
700        Ok(cfg)
701    }
702
703    /// Sync the inmemory representation to disk.
704    /// Takes a mutable reference because the saved file is a part of the `Config` state. This
705    /// protects from parallel calls resulting to a wrong file contents.
706    async fn sync(&mut self) -> Result<()> {
707        #[cfg(not(target_os = "ios"))]
708        ensure!(
709            !self
710                .lock_task
711                .as_ref()
712                .context("Config is read-only")?
713                .is_finished()
714        );
715
716        let tmp_path = self.file.with_extension("toml.tmp");
717        let mut file = fs::File::create(&tmp_path)
718            .await
719            .context("failed to create a tmp config")?;
720        file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
721            .await
722            .context("failed to write a tmp config")?;
723
724        // We use `sync_all()` and not `sync_data()` here.
725        // This translates to `fsync()` instead of `fdatasync()`.
726        // `fdatasync()` may be insufficient for newely created files
727        // and may not even synchronize the file size on some operating systems,
728        // resulting in a truncated file.
729        file.sync_all()
730            .await
731            .context("failed to sync a tmp config")?;
732        drop(file);
733        fs::rename(&tmp_path, &self.file)
734            .await
735            .context("failed to rename config")?;
736        // Sync the rename().
737        #[cfg(not(windows))]
738        {
739            let parent = self.file.parent().context("No parent directory")?;
740            let parent_file = fs::File::open(parent).await?;
741            parent_file.sync_all().await?;
742        }
743
744        Ok(())
745    }
746
747    /// Read a configuration from the given file into memory.
748    pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
749        let mut config = Self::new_nosync(file, writable).await?;
750        let bytes = fs::read(&config.file)
751            .await
752            .context("Failed to read file")?;
753        let s = std::str::from_utf8(&bytes)?;
754        config.inner = toml::from_str(s).context("Failed to parse config")?;
755
756        // Previous versions of the core stored absolute paths in account config.
757        // Convert them to relative paths.
758        let mut modified = false;
759        for account in &mut config.inner.accounts {
760            if account.dir.is_absolute()
761                && let Some(old_path_parent) = account.dir.parent()
762                && let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
763            {
764                account.dir = new_path.to_path_buf();
765                modified = true;
766            }
767        }
768        if modified && writable {
769            config.sync().await?;
770        }
771
772        Ok(config)
773    }
774
775    /// Loads all accounts defined in the configuration file.
776    ///
777    /// Created contexts share the same event channel and stock string
778    /// translations.
779    pub async fn load_accounts(
780        &self,
781        events: &Events,
782        stockstrings: &StockStrings,
783        push_subscriber: PushSubscriber,
784        dir: &Path,
785    ) -> Result<BTreeMap<u32, Context>> {
786        let mut accounts = BTreeMap::new();
787
788        for account_config in &self.inner.accounts {
789            let dbfile = account_config.dbfile(dir);
790            let ctx = ContextBuilder::new(dbfile.clone())
791                .with_id(account_config.id)
792                .with_events(events.clone())
793                .with_stock_strings(stockstrings.clone())
794                .with_push_subscriber(push_subscriber.clone())
795                .build()
796                .await
797                .with_context(|| format!("failed to create context from file {dbfile:?}"))?;
798            // Try to open without a passphrase,
799            // but do not return an error if account is passphare-protected.
800            ctx.open("".to_string()).await?;
801
802            accounts.insert(account_config.id, ctx);
803        }
804
805        Ok(accounts)
806    }
807
808    /// Creates a new account in the account manager directory.
809    #[expect(clippy::arithmetic_side_effects)]
810    async fn new_account(&mut self) -> Result<AccountConfig> {
811        let id = {
812            let id = self.inner.next_id;
813            let uuid = Uuid::new_v4();
814            let target_dir = PathBuf::from(uuid.to_string());
815
816            self.inner.accounts.push(AccountConfig {
817                id,
818                dir: target_dir,
819                uuid,
820            });
821            self.inner.next_id += 1;
822
823            // Add new account to the end of the order list
824            self.inner.accounts_order.push(id);
825
826            id
827        };
828
829        self.sync().await?;
830
831        self.select_account(id)
832            .await
833            .context("failed to select just added account")?;
834        let cfg = self
835            .get_account(id)
836            .context("failed to get just added account")?;
837        Ok(cfg)
838    }
839
840    /// Removes an existing account entirely.
841    pub async fn remove_account(&mut self, id: u32) -> Result<()> {
842        {
843            if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
844                // remove account from the configs
845                self.inner.accounts.remove(idx);
846            }
847
848            // Remove from order list as well
849            self.inner.accounts_order.retain(|&x| x != id);
850
851            if self.inner.selected_account == id {
852                // reset selected account
853                self.inner.selected_account = self
854                    .inner
855                    .accounts
856                    .first()
857                    .map(|e| e.id)
858                    .unwrap_or_default();
859            }
860        }
861
862        self.sync().await
863    }
864
865    /// Returns configuration file section for the given account ID.
866    fn get_account(&self, id: u32) -> Option<AccountConfig> {
867        self.inner.accounts.iter().find(|e| e.id == id).cloned()
868    }
869
870    /// Returns the ID of selected account.
871    pub fn get_selected_account(&self) -> u32 {
872        self.inner.selected_account
873    }
874
875    /// Changes selected account ID.
876    pub async fn select_account(&mut self, id: u32) -> Result<()> {
877        {
878            ensure!(
879                self.inner.accounts.iter().any(|e| e.id == id),
880                "invalid account id: {id}"
881            );
882
883            self.inner.selected_account = id;
884        }
885
886        self.sync().await?;
887        Ok(())
888    }
889}
890
891/// Spend up to 1 minute trying to do the operation.
892///
893/// Even if Delta Chat itself does not hold the file lock,
894/// there may be other processes such as antivirus,
895/// or the filesystem may be network-mounted.
896///
897/// Without this workaround removing account may fail on Windows with an error
898/// "The process cannot access the file because it is being used by another process. (os error 32)".
899#[expect(clippy::arithmetic_side_effects)]
900async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
901where
902    F: Fn() -> Fut,
903    Fut: Future<Output = std::result::Result<(), T>>,
904{
905    let mut counter = 0;
906    loop {
907        counter += 1;
908
909        if let Err(err) = f().await {
910            if counter > 60 {
911                return Err(err);
912            }
913
914            // Wait 1 second and try again.
915            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
916        } else {
917            break;
918        }
919    }
920    Ok(())
921}
922
923/// Configuration of a single account.
924#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
925struct AccountConfig {
926    /// Unique id.
927    pub id: u32,
928
929    /// Root directory for all data for this account.
930    ///
931    /// The path is relative to the account manager directory.
932    pub dir: std::path::PathBuf,
933
934    /// Universally unique account identifier.
935    pub uuid: Uuid,
936}
937
938impl AccountConfig {
939    /// Get the canonical dbfile name for this configuration.
940    pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
941        accounts_dir.join(&self.dir).join(DB_NAME)
942    }
943}
944
945#[cfg(test)]
946mod tests {
947    use super::*;
948    use crate::stock_str::{self, StockMessage};
949
950    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
951    async fn test_account_new_open() {
952        let dir = tempfile::tempdir().unwrap();
953        let p: PathBuf = dir.path().join("accounts1");
954
955        {
956            let writable = true;
957            let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
958            accounts.add_account().await.unwrap();
959
960            assert_eq!(accounts.accounts.len(), 1);
961            assert_eq!(accounts.config.get_selected_account(), 1);
962        }
963        for writable in [true, false] {
964            let accounts = Accounts::new(p.clone(), writable).await.unwrap();
965
966            assert_eq!(accounts.accounts.len(), 1);
967            assert_eq!(accounts.config.get_selected_account(), 1);
968        }
969    }
970
971    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
972    async fn test_account_new_empty_existing_dir() {
973        let dir = tempfile::tempdir().unwrap();
974        let p: PathBuf = dir.path().join("accounts");
975
976        // A non-empty directory without accounts.toml should fail.
977        fs::create_dir_all(&p).await.unwrap();
978        fs::write(p.join("stray_file.txt"), b"hello").await.unwrap();
979        assert!(Accounts::new(p.clone(), true).await.is_err());
980
981        // Clean up to an empty directory.
982        fs::remove_file(p.join("stray_file.txt")).await.unwrap();
983
984        // An empty directory without accounts.toml should succeed.
985        let mut accounts = Accounts::new(p.clone(), true).await.unwrap();
986        assert_eq!(accounts.accounts.len(), 0);
987        let id = accounts.add_account().await.unwrap();
988        assert_eq!(id, 1);
989    }
990
991    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
992    async fn test_account_new_open_conflict() {
993        let dir = tempfile::tempdir().unwrap();
994        let p: PathBuf = dir.path().join("accounts");
995        let writable = true;
996        let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
997
998        let writable = true;
999        assert!(Accounts::new(p.clone(), writable).await.is_err());
1000
1001        let writable = false;
1002        let accounts = Accounts::new(p, writable).await.unwrap();
1003        assert_eq!(accounts.accounts.len(), 0);
1004        assert_eq!(accounts.config.get_selected_account(), 0);
1005    }
1006
1007    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1008    async fn test_account_new_add_remove() {
1009        let dir = tempfile::tempdir().unwrap();
1010        let p: PathBuf = dir.path().join("accounts");
1011
1012        let writable = true;
1013        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1014        assert_eq!(accounts.accounts.len(), 0);
1015        assert_eq!(accounts.config.get_selected_account(), 0);
1016
1017        let id = accounts.add_account().await.unwrap();
1018        assert_eq!(id, 1);
1019        assert_eq!(accounts.accounts.len(), 1);
1020        assert_eq!(accounts.config.get_selected_account(), 1);
1021
1022        let id = accounts.add_account().await.unwrap();
1023        assert_eq!(id, 2);
1024        assert_eq!(accounts.config.get_selected_account(), id);
1025        assert_eq!(accounts.accounts.len(), 2);
1026
1027        accounts.select_account(1).await.unwrap();
1028        assert_eq!(accounts.config.get_selected_account(), 1);
1029
1030        accounts.remove_account(1).await.unwrap();
1031        assert_eq!(accounts.config.get_selected_account(), 2);
1032        assert_eq!(accounts.accounts.len(), 1);
1033    }
1034
1035    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1036    async fn test_accounts_remove_last() -> Result<()> {
1037        let dir = tempfile::tempdir()?;
1038        let p: PathBuf = dir.path().join("accounts");
1039
1040        let writable = true;
1041        let mut accounts = Accounts::new(p.clone(), writable).await?;
1042        assert!(accounts.get_selected_account().is_none());
1043        assert_eq!(accounts.config.get_selected_account(), 0);
1044
1045        let id = accounts.add_account().await?;
1046        assert!(accounts.get_selected_account().is_some());
1047        assert_eq!(id, 1);
1048        assert_eq!(accounts.accounts.len(), 1);
1049        assert_eq!(accounts.config.get_selected_account(), id);
1050
1051        accounts.remove_account(id).await?;
1052        assert!(accounts.get_selected_account().is_none());
1053
1054        Ok(())
1055    }
1056
1057    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1058    async fn test_migrate_account() {
1059        let dir = tempfile::tempdir().unwrap();
1060        let p: PathBuf = dir.path().join("accounts");
1061
1062        let writable = true;
1063        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1064        assert_eq!(accounts.accounts.len(), 0);
1065        assert_eq!(accounts.config.get_selected_account(), 0);
1066
1067        let extern_dbfile: PathBuf = dir.path().join("other");
1068        let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
1069            .await
1070            .unwrap();
1071        ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
1072            .await
1073            .unwrap();
1074
1075        drop(ctx);
1076
1077        accounts
1078            .migrate_account(extern_dbfile.clone())
1079            .await
1080            .unwrap();
1081        assert_eq!(accounts.accounts.len(), 1);
1082        assert_eq!(accounts.config.get_selected_account(), 1);
1083
1084        let ctx = accounts.get_selected_account().unwrap();
1085        assert_eq!(
1086            "me@mail.com",
1087            ctx.get_config(crate::config::Config::Addr)
1088                .await
1089                .unwrap()
1090                .unwrap()
1091        );
1092    }
1093
1094    /// Tests that accounts are sorted by ID.
1095    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1096    async fn test_accounts_sorted() {
1097        let dir = tempfile::tempdir().unwrap();
1098        let p: PathBuf = dir.path().join("accounts");
1099
1100        let writable = true;
1101        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
1102
1103        for expected_id in 1..10 {
1104            let id = accounts.add_account().await.unwrap();
1105            assert_eq!(id, expected_id);
1106        }
1107
1108        let ids = accounts.get_all();
1109        for (i, expected_id) in (1..10).enumerate() {
1110            assert_eq!(ids.get(i), Some(&expected_id));
1111        }
1112    }
1113
1114    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1115    async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
1116        let dir = tempfile::tempdir()?;
1117        let p: PathBuf = dir.path().join("accounts");
1118        let dummy_accounts = 10;
1119
1120        let (id0, id1, id2) = {
1121            let writable = true;
1122            let mut accounts = Accounts::new(p.clone(), writable).await?;
1123            accounts.add_account().await?;
1124            let ids = accounts.get_all();
1125            assert_eq!(ids.len(), 1);
1126
1127            let id0 = *ids.first().unwrap();
1128            let ctx = accounts.get_account(id0).unwrap();
1129            ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
1130                .await?;
1131
1132            let id1 = accounts.add_account().await?;
1133            let ctx = accounts.get_account(id1).unwrap();
1134            ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
1135                .await?;
1136
1137            // add and remove some accounts and force a gap (ids must not be reused)
1138            for _ in 0..dummy_accounts {
1139                let to_delete = accounts.add_account().await?;
1140                accounts.remove_account(to_delete).await?;
1141            }
1142
1143            let id2 = accounts.add_account().await?;
1144            let ctx = accounts.get_account(id2).unwrap();
1145            ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
1146                .await?;
1147
1148            accounts.select_account(id1).await?;
1149
1150            (id0, id1, id2)
1151        };
1152        assert!(id0 > 0);
1153        assert!(id1 > id0);
1154        assert!(id2 > id1 + dummy_accounts);
1155
1156        let (id0_reopened, id1_reopened, id2_reopened) = {
1157            let writable = false;
1158            let accounts = Accounts::new(p.clone(), writable).await?;
1159            let ctx = accounts.get_selected_account().unwrap();
1160            assert_eq!(
1161                ctx.get_config(crate::config::Config::Addr).await?,
1162                Some("two@example.org".to_string())
1163            );
1164
1165            let ids = accounts.get_all();
1166            assert_eq!(ids.len(), 3);
1167
1168            let id0 = *ids.first().unwrap();
1169            let ctx = accounts.get_account(id0).unwrap();
1170            assert_eq!(
1171                ctx.get_config(crate::config::Config::Addr).await?,
1172                Some("one@example.org".to_string())
1173            );
1174
1175            let id1 = *ids.get(1).unwrap();
1176            let t = accounts.get_account(id1).unwrap();
1177            assert_eq!(
1178                t.get_config(crate::config::Config::Addr).await?,
1179                Some("two@example.org".to_string())
1180            );
1181
1182            let id2 = *ids.get(2).unwrap();
1183            let ctx = accounts.get_account(id2).unwrap();
1184            assert_eq!(
1185                ctx.get_config(crate::config::Config::Addr).await?,
1186                Some("three@example.org".to_string())
1187            );
1188
1189            (id0, id1, id2)
1190        };
1191        assert_eq!(id0, id0_reopened);
1192        assert_eq!(id1, id1_reopened);
1193        assert_eq!(id2, id2_reopened);
1194
1195        Ok(())
1196    }
1197
1198    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1199    async fn test_no_accounts_event_emitter() -> Result<()> {
1200        let dir = tempfile::tempdir().unwrap();
1201        let p: PathBuf = dir.path().join("accounts");
1202
1203        let writable = true;
1204        let accounts = Accounts::new(p.clone(), writable).await?;
1205
1206        // Make sure there are no accounts.
1207        assert_eq!(accounts.accounts.len(), 0);
1208
1209        // Create event emitter.
1210        let event_emitter = accounts.get_event_emitter();
1211
1212        // Test that event emitter does not return `None` immediately.
1213        let duration = std::time::Duration::from_millis(1);
1214        assert!(
1215            tokio::time::timeout(duration, event_emitter.recv())
1216                .await
1217                .is_err()
1218        );
1219
1220        // When account manager is dropped, event emitter is exhausted.
1221        drop(accounts);
1222        assert_eq!(event_emitter.recv().await, None);
1223
1224        Ok(())
1225    }
1226
1227    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1228    async fn test_encrypted_account() -> Result<()> {
1229        let dir = tempfile::tempdir().context("failed to create tempdir")?;
1230        let p: PathBuf = dir.path().join("accounts");
1231
1232        let writable = true;
1233        let mut accounts = Accounts::new(p.clone(), writable)
1234            .await
1235            .context("failed to create accounts manager")?;
1236
1237        assert_eq!(accounts.accounts.len(), 0);
1238        let account_id = accounts
1239            .add_closed_account()
1240            .await
1241            .context("failed to add closed account")?;
1242        let account = accounts
1243            .get_selected_account()
1244            .context("failed to get account")?;
1245        assert_eq!(account.id, account_id);
1246        let passphrase_set_success = account
1247            .open("foobar".to_string())
1248            .await
1249            .context("failed to set passphrase")?;
1250        assert!(passphrase_set_success);
1251        drop(accounts);
1252
1253        let writable = false;
1254        let accounts = Accounts::new(p.clone(), writable)
1255            .await
1256            .context("failed to create second accounts manager")?;
1257        let account = accounts
1258            .get_selected_account()
1259            .context("failed to get account")?;
1260        assert_eq!(account.is_open().await, false);
1261
1262        // Try wrong passphrase.
1263        assert_eq!(account.open("barfoo".to_string()).await?, false);
1264        assert_eq!(account.open("".to_string()).await?, false);
1265
1266        assert_eq!(account.open("foobar".to_string()).await?, true);
1267        assert_eq!(account.is_open().await, true);
1268
1269        Ok(())
1270    }
1271
1272    /// Tests that accounts share stock string translations.
1273    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1274    async fn test_accounts_share_translations() -> Result<()> {
1275        let dir = tempfile::tempdir().unwrap();
1276        let p: PathBuf = dir.path().join("accounts");
1277
1278        let writable = true;
1279        let mut accounts = Accounts::new(p.clone(), writable).await?;
1280        accounts.add_account().await?;
1281        accounts.add_account().await?;
1282
1283        let account1 = accounts.get_account(1).context("failed to get account 1")?;
1284        let account2 = accounts.get_account(2).context("failed to get account 2")?;
1285
1286        assert_eq!(stock_str::no_messages(&account1), "No messages.");
1287        assert_eq!(stock_str::no_messages(&account2), "No messages.");
1288        account1.set_stock_translation(StockMessage::NoMessages, "foobar".to_string())?;
1289        assert_eq!(stock_str::no_messages(&account1), "foobar");
1290        assert_eq!(stock_str::no_messages(&account2), "foobar");
1291
1292        Ok(())
1293    }
1294}