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