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