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