deltachat/
accounts.rs

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