Skip to main content

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