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