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(), "{:?} does not exist", config_file);
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: {}",
728                id
729            );
730
731            self.inner.selected_account = id;
732        }
733
734        self.sync().await?;
735        Ok(())
736    }
737}
738
739/// Spend up to 1 minute trying to do the operation.
740///
741/// Even if Delta Chat itself does not hold the file lock,
742/// there may be other processes such as antivirus,
743/// or the filesystem may be network-mounted.
744///
745/// Without this workaround removing account may fail on Windows with an error
746/// "The process cannot access the file because it is being used by another process. (os error 32)".
747async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
748where
749    F: Fn() -> Fut,
750    Fut: Future<Output = std::result::Result<(), T>>,
751{
752    let mut counter = 0;
753    loop {
754        counter += 1;
755
756        if let Err(err) = f().await {
757            if counter > 60 {
758                return Err(err);
759            }
760
761            // Wait 1 second and try again.
762            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
763        } else {
764            break;
765        }
766    }
767    Ok(())
768}
769
770/// Configuration of a single account.
771#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
772struct AccountConfig {
773    /// Unique id.
774    pub id: u32,
775
776    /// Root directory for all data for this account.
777    ///
778    /// The path is relative to the account manager directory.
779    pub dir: std::path::PathBuf,
780
781    /// Universally unique account identifier.
782    pub uuid: Uuid,
783}
784
785impl AccountConfig {
786    /// Get the canonical dbfile name for this configuration.
787    pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf {
788        accounts_dir.join(&self.dir).join(DB_NAME)
789    }
790}
791
792#[cfg(test)]
793mod tests {
794    use super::*;
795    use crate::stock_str::{self, StockMessage};
796
797    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
798    async fn test_account_new_open() {
799        let dir = tempfile::tempdir().unwrap();
800        let p: PathBuf = dir.path().join("accounts1");
801
802        {
803            let writable = true;
804            let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
805            accounts.add_account().await.unwrap();
806
807            assert_eq!(accounts.accounts.len(), 1);
808            assert_eq!(accounts.config.get_selected_account(), 1);
809        }
810        for writable in [true, false] {
811            let accounts = Accounts::new(p.clone(), writable).await.unwrap();
812
813            assert_eq!(accounts.accounts.len(), 1);
814            assert_eq!(accounts.config.get_selected_account(), 1);
815        }
816    }
817
818    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
819    async fn test_account_new_open_conflict() {
820        let dir = tempfile::tempdir().unwrap();
821        let p: PathBuf = dir.path().join("accounts");
822        let writable = true;
823        let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
824
825        let writable = true;
826        assert!(Accounts::new(p.clone(), writable).await.is_err());
827
828        let writable = false;
829        let accounts = Accounts::new(p, writable).await.unwrap();
830        assert_eq!(accounts.accounts.len(), 0);
831        assert_eq!(accounts.config.get_selected_account(), 0);
832    }
833
834    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
835    async fn test_account_new_add_remove() {
836        let dir = tempfile::tempdir().unwrap();
837        let p: PathBuf = dir.path().join("accounts");
838
839        let writable = true;
840        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
841        assert_eq!(accounts.accounts.len(), 0);
842        assert_eq!(accounts.config.get_selected_account(), 0);
843
844        let id = accounts.add_account().await.unwrap();
845        assert_eq!(id, 1);
846        assert_eq!(accounts.accounts.len(), 1);
847        assert_eq!(accounts.config.get_selected_account(), 1);
848
849        let id = accounts.add_account().await.unwrap();
850        assert_eq!(id, 2);
851        assert_eq!(accounts.config.get_selected_account(), id);
852        assert_eq!(accounts.accounts.len(), 2);
853
854        accounts.select_account(1).await.unwrap();
855        assert_eq!(accounts.config.get_selected_account(), 1);
856
857        accounts.remove_account(1).await.unwrap();
858        assert_eq!(accounts.config.get_selected_account(), 2);
859        assert_eq!(accounts.accounts.len(), 1);
860    }
861
862    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
863    async fn test_accounts_remove_last() -> Result<()> {
864        let dir = tempfile::tempdir()?;
865        let p: PathBuf = dir.path().join("accounts");
866
867        let writable = true;
868        let mut accounts = Accounts::new(p.clone(), writable).await?;
869        assert!(accounts.get_selected_account().is_none());
870        assert_eq!(accounts.config.get_selected_account(), 0);
871
872        let id = accounts.add_account().await?;
873        assert!(accounts.get_selected_account().is_some());
874        assert_eq!(id, 1);
875        assert_eq!(accounts.accounts.len(), 1);
876        assert_eq!(accounts.config.get_selected_account(), id);
877
878        accounts.remove_account(id).await?;
879        assert!(accounts.get_selected_account().is_none());
880
881        Ok(())
882    }
883
884    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
885    async fn test_migrate_account() {
886        let dir = tempfile::tempdir().unwrap();
887        let p: PathBuf = dir.path().join("accounts");
888
889        let writable = true;
890        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
891        assert_eq!(accounts.accounts.len(), 0);
892        assert_eq!(accounts.config.get_selected_account(), 0);
893
894        let extern_dbfile: PathBuf = dir.path().join("other");
895        let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
896            .await
897            .unwrap();
898        ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
899            .await
900            .unwrap();
901
902        drop(ctx);
903
904        accounts
905            .migrate_account(extern_dbfile.clone())
906            .await
907            .unwrap();
908        assert_eq!(accounts.accounts.len(), 1);
909        assert_eq!(accounts.config.get_selected_account(), 1);
910
911        let ctx = accounts.get_selected_account().unwrap();
912        assert_eq!(
913            "me@mail.com",
914            ctx.get_config(crate::config::Config::Addr)
915                .await
916                .unwrap()
917                .unwrap()
918        );
919    }
920
921    /// Tests that accounts are sorted by ID.
922    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
923    async fn test_accounts_sorted() {
924        let dir = tempfile::tempdir().unwrap();
925        let p: PathBuf = dir.path().join("accounts");
926
927        let writable = true;
928        let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
929
930        for expected_id in 1..10 {
931            let id = accounts.add_account().await.unwrap();
932            assert_eq!(id, expected_id);
933        }
934
935        let ids = accounts.get_all();
936        for (i, expected_id) in (1..10).enumerate() {
937            assert_eq!(ids.get(i), Some(&expected_id));
938        }
939    }
940
941    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
942    async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
943        let dir = tempfile::tempdir()?;
944        let p: PathBuf = dir.path().join("accounts");
945        let dummy_accounts = 10;
946
947        let (id0, id1, id2) = {
948            let writable = true;
949            let mut accounts = Accounts::new(p.clone(), writable).await?;
950            accounts.add_account().await?;
951            let ids = accounts.get_all();
952            assert_eq!(ids.len(), 1);
953
954            let id0 = *ids.first().unwrap();
955            let ctx = accounts.get_account(id0).unwrap();
956            ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
957                .await?;
958
959            let id1 = accounts.add_account().await?;
960            let ctx = accounts.get_account(id1).unwrap();
961            ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
962                .await?;
963
964            // add and remove some accounts and force a gap (ids must not be reused)
965            for _ in 0..dummy_accounts {
966                let to_delete = accounts.add_account().await?;
967                accounts.remove_account(to_delete).await?;
968            }
969
970            let id2 = accounts.add_account().await?;
971            let ctx = accounts.get_account(id2).unwrap();
972            ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
973                .await?;
974
975            accounts.select_account(id1).await?;
976
977            (id0, id1, id2)
978        };
979        assert!(id0 > 0);
980        assert!(id1 > id0);
981        assert!(id2 > id1 + dummy_accounts);
982
983        let (id0_reopened, id1_reopened, id2_reopened) = {
984            let writable = false;
985            let accounts = Accounts::new(p.clone(), writable).await?;
986            let ctx = accounts.get_selected_account().unwrap();
987            assert_eq!(
988                ctx.get_config(crate::config::Config::Addr).await?,
989                Some("two@example.org".to_string())
990            );
991
992            let ids = accounts.get_all();
993            assert_eq!(ids.len(), 3);
994
995            let id0 = *ids.first().unwrap();
996            let ctx = accounts.get_account(id0).unwrap();
997            assert_eq!(
998                ctx.get_config(crate::config::Config::Addr).await?,
999                Some("one@example.org".to_string())
1000            );
1001
1002            let id1 = *ids.get(1).unwrap();
1003            let t = accounts.get_account(id1).unwrap();
1004            assert_eq!(
1005                t.get_config(crate::config::Config::Addr).await?,
1006                Some("two@example.org".to_string())
1007            );
1008
1009            let id2 = *ids.get(2).unwrap();
1010            let ctx = accounts.get_account(id2).unwrap();
1011            assert_eq!(
1012                ctx.get_config(crate::config::Config::Addr).await?,
1013                Some("three@example.org".to_string())
1014            );
1015
1016            (id0, id1, id2)
1017        };
1018        assert_eq!(id0, id0_reopened);
1019        assert_eq!(id1, id1_reopened);
1020        assert_eq!(id2, id2_reopened);
1021
1022        Ok(())
1023    }
1024
1025    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1026    async fn test_no_accounts_event_emitter() -> Result<()> {
1027        let dir = tempfile::tempdir().unwrap();
1028        let p: PathBuf = dir.path().join("accounts");
1029
1030        let writable = true;
1031        let accounts = Accounts::new(p.clone(), writable).await?;
1032
1033        // Make sure there are no accounts.
1034        assert_eq!(accounts.accounts.len(), 0);
1035
1036        // Create event emitter.
1037        let event_emitter = accounts.get_event_emitter();
1038
1039        // Test that event emitter does not return `None` immediately.
1040        let duration = std::time::Duration::from_millis(1);
1041        assert!(
1042            tokio::time::timeout(duration, event_emitter.recv())
1043                .await
1044                .is_err()
1045        );
1046
1047        // When account manager is dropped, event emitter is exhausted.
1048        drop(accounts);
1049        assert_eq!(event_emitter.recv().await, None);
1050
1051        Ok(())
1052    }
1053
1054    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1055    async fn test_encrypted_account() -> Result<()> {
1056        let dir = tempfile::tempdir().context("failed to create tempdir")?;
1057        let p: PathBuf = dir.path().join("accounts");
1058
1059        let writable = true;
1060        let mut accounts = Accounts::new(p.clone(), writable)
1061            .await
1062            .context("failed to create accounts manager")?;
1063
1064        assert_eq!(accounts.accounts.len(), 0);
1065        let account_id = accounts
1066            .add_closed_account()
1067            .await
1068            .context("failed to add closed account")?;
1069        let account = accounts
1070            .get_selected_account()
1071            .context("failed to get account")?;
1072        assert_eq!(account.id, account_id);
1073        let passphrase_set_success = account
1074            .open("foobar".to_string())
1075            .await
1076            .context("failed to set passphrase")?;
1077        assert!(passphrase_set_success);
1078        drop(accounts);
1079
1080        let writable = false;
1081        let accounts = Accounts::new(p.clone(), writable)
1082            .await
1083            .context("failed to create second accounts manager")?;
1084        let account = accounts
1085            .get_selected_account()
1086            .context("failed to get account")?;
1087        assert_eq!(account.is_open().await, false);
1088
1089        // Try wrong passphrase.
1090        assert_eq!(account.open("barfoo".to_string()).await?, false);
1091        assert_eq!(account.open("".to_string()).await?, false);
1092
1093        assert_eq!(account.open("foobar".to_string()).await?, true);
1094        assert_eq!(account.is_open().await, true);
1095
1096        Ok(())
1097    }
1098
1099    /// Tests that accounts share stock string translations.
1100    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1101    async fn test_accounts_share_translations() -> Result<()> {
1102        let dir = tempfile::tempdir().unwrap();
1103        let p: PathBuf = dir.path().join("accounts");
1104
1105        let writable = true;
1106        let mut accounts = Accounts::new(p.clone(), writable).await?;
1107        accounts.add_account().await?;
1108        accounts.add_account().await?;
1109
1110        let account1 = accounts.get_account(1).context("failed to get account 1")?;
1111        let account2 = accounts.get_account(2).context("failed to get account 2")?;
1112
1113        assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
1114        assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
1115        account1
1116            .set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
1117            .await?;
1118        assert_eq!(stock_str::no_messages(&account1).await, "foobar");
1119        assert_eq!(stock_str::no_messages(&account2).await, "foobar");
1120
1121        Ok(())
1122    }
1123}