deltachat/
accounts.rs

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