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