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