deltachat/
configure.rs

1//! # Email accounts autoconfiguration process.
2//!
3//! The module provides automatic lookup of configuration
4//! for email providers based on the built-in [provider database],
5//! [Mozilla Thunderbird Autoconfiguration protocol]
6//! and [Outlook's Autodiscover].
7//!
8//! [provider database]: crate::provider
9//! [Mozilla Thunderbird Autoconfiguration protocol]: auto_mozilla
10//! [Outlook's Autodiscover]: auto_outlook
11
12mod auto_mozilla;
13mod auto_outlook;
14pub(crate) mod server_params;
15
16use anyhow::{Context as _, Result, bail, ensure, format_err};
17use auto_mozilla::moz_autoconfigure;
18use auto_outlook::outlk_autodiscover;
19use deltachat_contact_tools::{EmailAddress, addr_normalize};
20use futures::FutureExt;
21use futures_lite::FutureExt as _;
22use percent_encoding::utf8_percent_encode;
23use server_params::{ServerParams, expand_param_vector};
24use tokio::task;
25
26use crate::config::Config;
27use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
28use crate::context::Context;
29use crate::imap::Imap;
30use crate::log::warn;
31use crate::login_param::EnteredCertificateChecks;
32pub use crate::login_param::EnteredLoginParam;
33use crate::message::Message;
34use crate::net::proxy::ProxyConfig;
35use crate::oauth2::get_oauth2_addr;
36use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
37use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
38use crate::smtp::Smtp;
39use crate::sync::Sync::*;
40use crate::tools::time;
41use crate::transport::{
42    ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
43    ConnectionCandidate, send_sync_transports,
44};
45use crate::{EventType, stock_str};
46use crate::{chat, provider};
47
48/// Maximum number of relays
49/// see <https://github.com/chatmail/core/issues/7608>
50pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5;
51
52macro_rules! progress {
53    ($context:tt, $progress:expr, $comment:expr) => {
54        assert!(
55            $progress <= 1000,
56            "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
57        );
58        $context.emit_event($crate::events::EventType::ConfigureProgress {
59            progress: $progress,
60            comment: $comment,
61        });
62    };
63    ($context:tt, $progress:expr) => {
64        progress!($context, $progress, None);
65    };
66}
67
68impl Context {
69    /// Checks if the context is already configured.
70    pub async fn is_configured(&self) -> Result<bool> {
71        self.sql.exists("SELECT COUNT(*) FROM transports", ()).await
72    }
73
74    /// Configures this account with the currently provided parameters.
75    ///
76    /// Deprecated since 2025-02; use `add_transport_from_qr()`
77    /// or `add_or_update_transport()` instead.
78    pub async fn configure(&self) -> Result<()> {
79        let mut param = EnteredLoginParam::load(self).await?;
80
81        self.add_transport_inner(&mut param).await
82    }
83
84    /// Configures a new email account using the provided parameters
85    /// and adds it as a transport.
86    ///
87    /// If the email address is the same as an existing transport,
88    /// then this existing account will be reconfigured instead of a new one being added.
89    ///
90    /// This function stops and starts IO as needed.
91    ///
92    /// Usually it will be enough to only set `addr` and `imap.password`,
93    /// and all the other settings will be autoconfigured.
94    ///
95    /// During configuration, ConfigureProgress events are emitted;
96    /// they indicate a successful configuration as well as errors
97    /// and may be used to create a progress bar.
98    /// This function will return after configuration is finished.
99    ///
100    /// If configuration is successful,
101    /// the working server parameters will be saved
102    /// and used for connecting to the server.
103    /// The parameters entered by the user will be saved separately
104    /// so that they can be prefilled when the user opens the server-configuration screen again.
105    ///
106    /// See also:
107    /// - [Self::is_configured()] to check whether there is
108    ///   at least one working transport.
109    /// - [Self::add_transport_from_qr()] to add a transport
110    ///   from a server encoded in a QR code.
111    /// - [Self::list_transports()] to get a list of all configured transports.
112    /// - [Self::delete_transport()] to remove a transport.
113    pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
114        self.stop_io().await;
115        let result = self.add_transport_inner(param).await;
116        if result.is_err() {
117            if let Ok(true) = self.is_configured().await {
118                self.start_io().await;
119            }
120            return result;
121        }
122        self.start_io().await;
123        Ok(())
124    }
125
126    pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
127        ensure!(
128            !self.scheduler.is_running().await,
129            "cannot configure, already running"
130        );
131        ensure!(
132            self.sql.is_open().await,
133            "cannot configure, database not opened."
134        );
135        param.addr = addr_normalize(&param.addr);
136        let cancel_channel = self.alloc_ongoing().await?;
137
138        let res = self
139            .inner_configure(param)
140            .race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
141            .await;
142
143        self.free_ongoing().await;
144
145        if let Err(err) = res.as_ref() {
146            // We are using Anyhow's .context() and to show the
147            // inner error, too, we need the {:#}:
148            let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await;
149            progress!(self, 0, Some(error_msg.clone()));
150            bail!(error_msg);
151        } else {
152            param.save(self).await?;
153            progress!(self, 1000);
154        }
155
156        res
157    }
158
159    /// Adds a new email account as a transport
160    /// using the server encoded in the QR code.
161    /// See [Self::add_or_update_transport].
162    pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
163        self.stop_io().await;
164
165        let result = async move {
166            let mut param = match crate::qr::check_qr(self, qr).await? {
167                crate::qr::Qr::Account { .. } => login_param_from_account_qr(self, qr).await?,
168                crate::qr::Qr::Login { address, options } => {
169                    login_param_from_login_qr(&address, options)?
170                }
171                _ => bail!("QR code does not contain account"),
172            };
173            self.add_transport_inner(&mut param).await?;
174            Ok(())
175        }
176        .await;
177
178        if result.is_err() {
179            if let Ok(true) = self.is_configured().await {
180                self.start_io().await;
181            }
182            return result;
183        }
184        self.start_io().await;
185        Ok(())
186    }
187
188    /// Returns the list of all email accounts that are used as a transport in the current profile.
189    /// Use [Self::add_or_update_transport()] to add or change a transport
190    /// and [Self::delete_transport()] to delete a transport.
191    pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
192        let transports = self
193            .sql
194            .query_map_vec("SELECT entered_param FROM transports", (), |row| {
195                let entered_param: String = row.get(0)?;
196                let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?;
197                Ok(transport)
198            })
199            .await?;
200
201        Ok(transports)
202    }
203
204    /// Returns the number of configured transports.
205    pub async fn count_transports(&self) -> Result<usize> {
206        self.sql.count("SELECT COUNT(*) FROM transports", ()).await
207    }
208
209    /// Removes the transport with the specified email address
210    /// (i.e. [EnteredLoginParam::addr]).
211    pub async fn delete_transport(&self, addr: &str) -> Result<()> {
212        let now = time();
213        let removed_transport_id = self
214            .sql
215            .transaction(|transaction| {
216                let primary_addr = transaction.query_row(
217                    "SELECT value FROM config WHERE keyname='configured_addr'",
218                    (),
219                    |row| {
220                        let addr: String = row.get(0)?;
221                        Ok(addr)
222                    },
223                )?;
224
225                if primary_addr == addr {
226                    bail!("Cannot delete primary transport");
227                }
228                let (transport_id, add_timestamp) = transaction.query_row(
229                    "DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp",
230                    (addr,),
231                    |row| {
232                        let id: u32 = row.get(0)?;
233                        let add_timestamp: i64 = row.get(1)?;
234                        Ok((id, add_timestamp))
235                    },
236                )?;
237                transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
238                transaction.execute(
239                    "DELETE FROM imap_sync WHERE transport_id=?",
240                    (transport_id,),
241                )?;
242
243                // Removal timestamp should not be lower than addition timestamp
244                // to be accepted by other devices when synced.
245                let remove_timestamp = std::cmp::max(now, add_timestamp);
246
247                transaction.execute(
248                    "INSERT INTO removed_transports (addr, remove_timestamp)
249                     VALUES (?, ?)
250                     ON CONFLICT (addr)
251                     DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
252                    (addr, remove_timestamp),
253                )?;
254
255                Ok(transport_id)
256            })
257            .await?;
258        send_sync_transports(self).await?;
259        self.quota.write().await.remove(&removed_transport_id);
260
261        Ok(())
262    }
263
264    async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
265        info!(self, "Configure ...");
266
267        let old_addr = self.get_config(Config::ConfiguredAddr).await?;
268        if old_addr.is_some()
269            && !self
270                .sql
271                .exists(
272                    "SELECT COUNT(*) FROM transports WHERE addr=?",
273                    (&param.addr,),
274                )
275                .await?
276        {
277            // Should be checked before `MvboxMove` because the latter makes no sense in presense of
278            // `OnlyFetchMvbox` and even grayed out in the UIs in this case.
279            if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
280                bail!(
281                    "To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
282                );
283            }
284            if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
285                bail!(
286                    "To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
287                );
288            }
289
290            if self
291                .sql
292                .count("SELECT COUNT(*) FROM transports", ())
293                .await?
294                >= MAX_TRANSPORT_RELAYS
295            {
296                bail!(
297                    "You have reached the maximum number of relays ({}).",
298                    MAX_TRANSPORT_RELAYS
299                )
300            }
301        }
302
303        let provider = match configure(self, param).await {
304            Err(error) => {
305                // Log entered and actual params
306                let configured_param = get_configured_param(self, param).await;
307                warn!(
308                    self,
309                    "configure failed: Entered params: {}. Used params: {}. Error: {error}.",
310                    param.to_string(),
311                    configured_param
312                        .map(|param| param.to_string())
313                        .unwrap_or("error".to_owned())
314                );
315                return Err(error);
316            }
317            Ok(provider) => provider,
318        };
319        self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
320            .await?;
321        on_configure_completed(self, provider).await?;
322        Ok(())
323    }
324}
325
326async fn on_configure_completed(
327    context: &Context,
328    provider: Option<&'static Provider>,
329) -> Result<()> {
330    if let Some(provider) = provider {
331        if let Some(config_defaults) = provider.config_defaults {
332            for def in config_defaults {
333                if !context.config_exists(def.key).await? {
334                    info!(context, "apply config_defaults {}={}", def.key, def.value);
335                    context
336                        .set_config_ex(Nosync, def.key, Some(def.value))
337                        .await?;
338                } else {
339                    info!(
340                        context,
341                        "skip already set config_defaults {}={}", def.key, def.value
342                    );
343                }
344            }
345        }
346
347        if !provider.after_login_hint.is_empty() {
348            let mut msg = Message::new_text(provider.after_login_hint.to_string());
349            if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
350                .await
351                .is_err()
352            {
353                warn!(context, "cannot add after_login_hint as core-provider-info");
354            }
355        }
356    }
357
358    Ok(())
359}
360
361/// Retrieves data from autoconfig and provider database
362/// to transform user-entered login parameters into complete configuration.
363async fn get_configured_param(
364    ctx: &Context,
365    param: &EnteredLoginParam,
366) -> Result<ConfiguredLoginParam> {
367    ensure!(!param.addr.is_empty(), "Missing email address.");
368
369    ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password.");
370
371    // SMTP password is an "advanced" setting. If unset, use the same password as for IMAP.
372    let smtp_password = if param.smtp.password.is_empty() {
373        param.imap.password.clone()
374    } else {
375        param.smtp.password.clone()
376    };
377
378    let mut addr = param.addr.clone();
379    if param.oauth2 {
380        // the used oauth2 addr may differ, check this.
381        // if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
382        progress!(ctx, 10);
383        if let Some(oauth2_addr) = get_oauth2_addr(ctx, &param.addr, &param.imap.password)
384            .await?
385            .and_then(|e| e.parse().ok())
386        {
387            info!(ctx, "Authorized address is {}", oauth2_addr);
388            addr = oauth2_addr;
389            ctx.sql
390                .set_raw_config("addr", Some(param.addr.as_str()))
391                .await?;
392        }
393        progress!(ctx, 20);
394    }
395    // no oauth? - just continue it's no error
396
397    let parsed = EmailAddress::new(&param.addr).context("Bad email-address")?;
398    let param_domain = parsed.domain;
399
400    progress!(ctx, 200);
401
402    let provider;
403    let param_autoconfig;
404    if param.imap.server.is_empty()
405        && param.imap.port == 0
406        && param.imap.security == Socket::Automatic
407        && param.imap.user.is_empty()
408        && param.smtp.server.is_empty()
409        && param.smtp.port == 0
410        && param.smtp.security == Socket::Automatic
411        && param.smtp.user.is_empty()
412    {
413        // no advanced parameters entered by the user: query provider-database or do Autoconfig
414        info!(
415            ctx,
416            "checking internal provider-info for offline autoconfig"
417        );
418
419        provider = provider::get_provider_info(&param_domain);
420        if let Some(provider) = provider {
421            if provider.server.is_empty() {
422                info!(ctx, "Offline autoconfig found, but no servers defined.");
423                param_autoconfig = None;
424            } else {
425                info!(ctx, "Offline autoconfig found.");
426                let servers = provider
427                    .server
428                    .iter()
429                    .map(|s| ServerParams {
430                        protocol: s.protocol,
431                        socket: s.socket,
432                        hostname: s.hostname.to_string(),
433                        port: s.port,
434                        username: match s.username_pattern {
435                            UsernamePattern::Email => param.addr.to_string(),
436                            UsernamePattern::Emaillocalpart => {
437                                if let Some(at) = param.addr.find('@') {
438                                    param.addr.split_at(at).0.to_string()
439                                } else {
440                                    param.addr.to_string()
441                                }
442                            }
443                        },
444                    })
445                    .collect();
446
447                param_autoconfig = Some(servers)
448            }
449        } else {
450            // Try receiving autoconfig
451            info!(ctx, "No offline autoconfig found.");
452            param_autoconfig = get_autoconfig(ctx, param, &param_domain).await;
453        }
454    } else {
455        provider = None;
456        param_autoconfig = None;
457    }
458
459    progress!(ctx, 500);
460
461    let mut servers = param_autoconfig.unwrap_or_default();
462    if !servers
463        .iter()
464        .any(|server| server.protocol == Protocol::Imap)
465    {
466        servers.push(ServerParams {
467            protocol: Protocol::Imap,
468            hostname: param.imap.server.clone(),
469            port: param.imap.port,
470            socket: param.imap.security,
471            username: param.imap.user.clone(),
472        })
473    }
474    if !servers
475        .iter()
476        .any(|server| server.protocol == Protocol::Smtp)
477    {
478        servers.push(ServerParams {
479            protocol: Protocol::Smtp,
480            hostname: param.smtp.server.clone(),
481            port: param.smtp.port,
482            socket: param.smtp.security,
483            username: param.smtp.user.clone(),
484        })
485    }
486
487    let servers = expand_param_vector(servers, &param.addr, &param_domain);
488
489    let configured_login_param = ConfiguredLoginParam {
490        addr,
491        imap: servers
492            .iter()
493            .filter_map(|params| {
494                let Ok(security) = params.socket.try_into() else {
495                    return None;
496                };
497                if params.protocol == Protocol::Imap {
498                    Some(ConfiguredServerLoginParam {
499                        connection: ConnectionCandidate {
500                            host: params.hostname.clone(),
501                            port: params.port,
502                            security,
503                        },
504                        user: params.username.clone(),
505                    })
506                } else {
507                    None
508                }
509            })
510            .collect(),
511        imap_user: param.imap.user.clone(),
512        imap_password: param.imap.password.clone(),
513        smtp: servers
514            .iter()
515            .filter_map(|params| {
516                let Ok(security) = params.socket.try_into() else {
517                    return None;
518                };
519                if params.protocol == Protocol::Smtp {
520                    Some(ConfiguredServerLoginParam {
521                        connection: ConnectionCandidate {
522                            host: params.hostname.clone(),
523                            port: params.port,
524                            security,
525                        },
526                        user: params.username.clone(),
527                    })
528                } else {
529                    None
530                }
531            })
532            .collect(),
533        smtp_user: param.smtp.user.clone(),
534        smtp_password,
535        provider,
536        certificate_checks: match param.certificate_checks {
537            EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
538            EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
539            EnteredCertificateChecks::AcceptInvalidCertificates
540            | EnteredCertificateChecks::AcceptInvalidCertificates2 => {
541                ConfiguredCertificateChecks::AcceptInvalidCertificates
542            }
543        },
544        oauth2: param.oauth2,
545    };
546    Ok(configured_login_param)
547}
548
549async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
550    progress!(ctx, 1);
551
552    let configured_param = get_configured_param(ctx, param).await?;
553    let proxy_config = ProxyConfig::load(ctx).await?;
554    let strict_tls = configured_param.strict_tls(proxy_config.is_some());
555
556    progress!(ctx, 550);
557
558    // Spawn SMTP configuration task
559    // to try SMTP while connecting to IMAP.
560    let context_smtp = ctx.clone();
561    let smtp_param = configured_param.smtp.clone();
562    let smtp_password = configured_param.smtp_password.clone();
563    let smtp_addr = configured_param.addr.clone();
564
565    let proxy_config2 = proxy_config.clone();
566    let smtp_config_task = task::spawn(async move {
567        let mut smtp = Smtp::new();
568        smtp.connect(
569            &context_smtp,
570            &smtp_param,
571            &smtp_password,
572            &proxy_config2,
573            &smtp_addr,
574            strict_tls,
575            configured_param.oauth2,
576        )
577        .await?;
578
579        Ok::<(), anyhow::Error>(())
580    });
581
582    progress!(ctx, 600);
583
584    // Configure IMAP
585
586    let transport_id = 0;
587    let (_s, r) = async_channel::bounded(1);
588    let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
589    let configuring = true;
590    let imap_session = match imap.connect(ctx, configuring).await {
591        Ok(imap_session) => imap_session,
592        Err(err) => {
593            bail!(
594                "{}",
595                nicer_configuration_error(ctx, format!("{err:#}")).await
596            );
597        }
598    };
599
600    progress!(ctx, 850);
601
602    // Wait for SMTP configuration
603    smtp_config_task.await??;
604
605    progress!(ctx, 900);
606
607    let is_configured = ctx.is_configured().await?;
608    if !is_configured {
609        ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
610        ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
611    }
612    if !ctx.get_config_bool(Config::FixIsChatmail).await? {
613        if imap_session.is_chatmail() {
614            ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
615        } else if !is_configured {
616            // Reset the setting that may have been set
617            // during failed configuration.
618            ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
619        }
620    }
621
622    drop(imap_session);
623    drop(imap);
624
625    progress!(ctx, 910);
626
627    let provider = configured_param.provider;
628    configured_param
629        .clone()
630        .save_to_transports_table(ctx, param, time())
631        .await?;
632    send_sync_transports(ctx).await?;
633
634    ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
635        .await?;
636
637    progress!(ctx, 920);
638
639    ctx.scheduler.interrupt_inbox().await;
640
641    progress!(ctx, 940);
642    ctx.update_device_chats()
643        .await
644        .context("Failed to update device chats")?;
645
646    ctx.sql.set_raw_config_bool("configured", true).await?;
647    ctx.emit_event(EventType::AccountsItemChanged);
648
649    Ok(provider)
650}
651
652/// Retrieve available autoconfigurations.
653///
654/// A. Search configurations from the domain used in the email-address
655/// B. If we have no configuration yet, search configuration in Thunderbird's central database
656async fn get_autoconfig(
657    ctx: &Context,
658    param: &EnteredLoginParam,
659    param_domain: &str,
660) -> Option<Vec<ServerParams>> {
661    // Make sure to not encode `.` as `%2E` here.
662    // Some servers like murena.io on 2024-11-01 produce incorrect autoconfig XML
663    // when address is encoded.
664    // E.g.
665    // <https://autoconfig.murena.io/mail/config-v1.1.xml?emailaddress=foobar%40example%2Eorg>
666    // produced XML file with `<username>foobar@example%2Eorg</username>`
667    // resulting in failure to log in.
668    let param_addr_urlencoded =
669        utf8_percent_encode(&param.addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
670
671    if let Ok(res) = moz_autoconfigure(
672        ctx,
673        &format!(
674            "https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
675        ),
676        &param.addr,
677    )
678    .await
679    {
680        return Some(res);
681    }
682    progress!(ctx, 300);
683
684    if let Ok(res) = moz_autoconfigure(
685        ctx,
686        // the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>,  which makes some sense
687        &format!(
688            "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
689            &param_domain, &param_addr_urlencoded
690        ),
691        &param.addr,
692    )
693    .await
694    {
695        return Some(res);
696    }
697    progress!(ctx, 310);
698
699    // Outlook uses always SSL but different domains (this comment describes the next two steps)
700    if let Ok(res) = outlk_autodiscover(
701        ctx,
702        format!("https://{}/autodiscover/autodiscover.xml", &param_domain),
703    )
704    .await
705    {
706        return Some(res);
707    }
708    progress!(ctx, 320);
709
710    if let Ok(res) = outlk_autodiscover(
711        ctx,
712        format!(
713            "https://autodiscover.{}/autodiscover/autodiscover.xml",
714            &param_domain
715        ),
716    )
717    .await
718    {
719        return Some(res);
720    }
721    progress!(ctx, 330);
722
723    // always SSL for Thunderbird's database
724    if let Ok(res) = moz_autoconfigure(
725        ctx,
726        &format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
727        &param.addr,
728    )
729    .await
730    {
731        return Some(res);
732    }
733
734    None
735}
736
737async fn nicer_configuration_error(context: &Context, e: String) -> String {
738    if e.to_lowercase().contains("could not resolve")
739        || e.to_lowercase().contains("connection attempts")
740        || e.to_lowercase()
741            .contains("temporary failure in name resolution")
742        || e.to_lowercase().contains("name or service not known")
743        || e.to_lowercase()
744            .contains("failed to lookup address information")
745    {
746        return stock_str::error_no_network(context).await;
747    }
748
749    e
750}
751
752#[derive(Debug, thiserror::Error)]
753pub enum Error {
754    #[error("Invalid email address: {0:?}")]
755    InvalidEmailAddress(String),
756
757    #[error("XML error at position {position}: {error}")]
758    InvalidXml {
759        position: u64,
760        #[source]
761        error: quick_xml::Error,
762    },
763
764    #[error("Number of redirection is exceeded")]
765    Redirection,
766
767    #[error("{0:#}")]
768    Other(#[from] anyhow::Error),
769}
770
771#[cfg(test)]
772mod tests {
773    use super::*;
774    use crate::config::Config;
775    use crate::login_param::EnteredServerLoginParam;
776    use crate::test_utils::TestContext;
777
778    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
779    async fn test_no_panic_on_bad_credentials() {
780        let t = TestContext::new().await;
781        t.set_config(Config::Addr, Some("probably@unexistant.addr"))
782            .await
783            .unwrap();
784        t.set_config(Config::MailPw, Some("123456")).await.unwrap();
785        assert!(t.configure().await.is_err());
786    }
787
788    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
789    async fn test_get_configured_param() -> Result<()> {
790        let t = &TestContext::new().await;
791        let entered_param = EnteredLoginParam {
792            addr: "alice@example.org".to_string(),
793
794            imap: EnteredServerLoginParam {
795                user: "alice@example.net".to_string(),
796                password: "foobar".to_string(),
797                ..Default::default()
798            },
799
800            ..Default::default()
801        };
802        let configured_param = get_configured_param(t, &entered_param).await?;
803        assert_eq!(configured_param.imap_user, "alice@example.net");
804        assert_eq!(configured_param.smtp_user, "");
805        Ok(())
806    }
807}