deltachat/
accounts.rs

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