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