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(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(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
265        Ok(())
266    }
267
268    /// Change whether the transport is unpublished.
269    ///
270    /// Unpublished transports are not advertised to contacts,
271    /// and self-sent messages are not sent there,
272    /// so that we don't cause extra messages to the corresponding inbox,
273    /// but can still receive messages from contacts who don't know our new transport addresses yet.
274    ///
275    /// The default is false, but when the user updates from a version that didn't have this flag,
276    /// existing secondary transports are set to unpublished,
277    /// so that an existing transport address doesn't suddenly get spammed with a lot of messages.
278    pub async fn set_transport_unpublished(&self, addr: &str, unpublished: bool) -> Result<()> {
279        self.sql
280            .transaction(|trans| {
281                let primary_addr: String = trans
282                    .query_row(
283                        "SELECT value FROM config WHERE keyname='configured_addr'",
284                        (),
285                        |row| row.get(0),
286                    )
287                    .context("Select primary address")?;
288                if primary_addr == addr && unpublished {
289                    bail!("Can't set primary relay as unpublished");
290                }
291                // We need to update the timestamp so that the key's timestamp changes
292                // and is recognized as newer by our peers
293                trans
294                    .execute(
295                        "UPDATE transports SET is_published=?, add_timestamp=? WHERE addr=? AND is_published!=?1",
296                        (!unpublished, time(), addr),
297                    )
298                    .context("Update transports")?;
299                Ok(())
300            })
301            .await?;
302        send_sync_transports(self).await?;
303        Ok(())
304    }
305
306    async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
307        info!(self, "Configure ...");
308
309        let old_addr = self.get_config(Config::ConfiguredAddr).await?;
310        if old_addr.is_some()
311            && !self
312                .sql
313                .exists(
314                    "SELECT COUNT(*) FROM transports WHERE addr=?",
315                    (&param.addr,),
316                )
317                .await?
318        {
319            // Should be checked before `MvboxMove` because the latter makes no sense in presense of
320            // `OnlyFetchMvbox` and even grayed out in the UIs in this case.
321            if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
322                bail!(
323                    "To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
324                );
325            }
326            if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
327                bail!(
328                    "To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
329                );
330            }
331
332            if self
333                .sql
334                .count("SELECT COUNT(*) FROM transports", ())
335                .await?
336                >= MAX_TRANSPORT_RELAYS
337            {
338                bail!(
339                    "You have reached the maximum number of relays ({}).",
340                    MAX_TRANSPORT_RELAYS
341                )
342            }
343        }
344
345        let provider = match configure(self, param).await {
346            Err(error) => {
347                // Log entered and actual params
348                let configured_param = get_configured_param(self, param).await;
349                warn!(
350                    self,
351                    "configure failed: Entered params: {}. Used params: {}. Error: {error}.",
352                    param.to_string(),
353                    configured_param
354                        .map(|param| param.to_string())
355                        .unwrap_or("error".to_owned())
356                );
357                return Err(error);
358            }
359            Ok(provider) => provider,
360        };
361        self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
362            .await?;
363        on_configure_completed(self, provider).await?;
364        Ok(())
365    }
366}
367
368async fn on_configure_completed(
369    context: &Context,
370    provider: Option<&'static Provider>,
371) -> Result<()> {
372    if let Some(provider) = provider {
373        if let Some(config_defaults) = provider.config_defaults {
374            for def in config_defaults {
375                if !context.config_exists(def.key).await? {
376                    info!(context, "apply config_defaults {}={}", def.key, def.value);
377                    context
378                        .set_config_ex(Nosync, def.key, Some(def.value))
379                        .await?;
380                } else {
381                    info!(
382                        context,
383                        "skip already set config_defaults {}={}", def.key, def.value
384                    );
385                }
386            }
387        }
388
389        if !provider.after_login_hint.is_empty() {
390            let mut msg = Message::new_text(provider.after_login_hint.to_string());
391            if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
392                .await
393                .is_err()
394            {
395                warn!(context, "cannot add after_login_hint as core-provider-info");
396            }
397        }
398    }
399
400    Ok(())
401}
402
403/// Retrieves data from autoconfig and provider database
404/// to transform user-entered login parameters into complete configuration.
405async fn get_configured_param(
406    ctx: &Context,
407    param: &EnteredLoginParam,
408) -> Result<ConfiguredLoginParam> {
409    ensure!(!param.addr.is_empty(), "Missing email address.");
410
411    ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password.");
412
413    // SMTP password is an "advanced" setting. If unset, use the same password as for IMAP.
414    let smtp_password = if param.smtp.password.is_empty() {
415        param.imap.password.clone()
416    } else {
417        param.smtp.password.clone()
418    };
419
420    let mut addr = param.addr.clone();
421    if param.oauth2 {
422        // the used oauth2 addr may differ, check this.
423        // if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
424        progress!(ctx, 10);
425        if let Some(oauth2_addr) = get_oauth2_addr(ctx, &param.addr, &param.imap.password)
426            .await?
427            .and_then(|e| e.parse().ok())
428        {
429            info!(ctx, "Authorized address is {}", oauth2_addr);
430            addr = oauth2_addr;
431            ctx.sql
432                .set_raw_config("addr", Some(param.addr.as_str()))
433                .await?;
434        }
435        progress!(ctx, 20);
436    }
437    // no oauth? - just continue it's no error
438
439    let parsed = EmailAddress::new(&param.addr).context("Bad email-address")?;
440    let param_domain = parsed.domain;
441
442    progress!(ctx, 200);
443
444    let provider;
445    let param_autoconfig;
446    if param.imap.server.is_empty()
447        && param.imap.port == 0
448        && param.imap.security == Socket::Automatic
449        && param.imap.user.is_empty()
450        && param.smtp.server.is_empty()
451        && param.smtp.port == 0
452        && param.smtp.security == Socket::Automatic
453        && param.smtp.user.is_empty()
454    {
455        // no advanced parameters entered by the user: query provider-database or do Autoconfig
456        info!(
457            ctx,
458            "checking internal provider-info for offline autoconfig"
459        );
460
461        provider = provider::get_provider_info(&param_domain);
462        if let Some(provider) = provider {
463            if provider.server.is_empty() {
464                info!(ctx, "Offline autoconfig found, but no servers defined.");
465                param_autoconfig = None;
466            } else {
467                info!(ctx, "Offline autoconfig found.");
468                let servers = provider
469                    .server
470                    .iter()
471                    .map(|s| ServerParams {
472                        protocol: s.protocol,
473                        socket: s.socket,
474                        hostname: s.hostname.to_string(),
475                        port: s.port,
476                        username: match s.username_pattern {
477                            UsernamePattern::Email => param.addr.to_string(),
478                            UsernamePattern::Emaillocalpart => {
479                                if let Some(at) = param.addr.find('@') {
480                                    param.addr.split_at(at).0.to_string()
481                                } else {
482                                    param.addr.to_string()
483                                }
484                            }
485                        },
486                    })
487                    .collect();
488
489                param_autoconfig = Some(servers)
490            }
491        } else {
492            // Try receiving autoconfig
493            info!(ctx, "No offline autoconfig found.");
494            param_autoconfig = get_autoconfig(ctx, param, &param_domain).await;
495        }
496    } else {
497        provider = None;
498        param_autoconfig = None;
499    }
500
501    progress!(ctx, 500);
502
503    let mut servers = param_autoconfig.unwrap_or_default();
504    if !servers
505        .iter()
506        .any(|server| server.protocol == Protocol::Imap)
507    {
508        servers.push(ServerParams {
509            protocol: Protocol::Imap,
510            hostname: param.imap.server.clone(),
511            port: param.imap.port,
512            socket: param.imap.security,
513            username: param.imap.user.clone(),
514        })
515    }
516    if !servers
517        .iter()
518        .any(|server| server.protocol == Protocol::Smtp)
519    {
520        servers.push(ServerParams {
521            protocol: Protocol::Smtp,
522            hostname: param.smtp.server.clone(),
523            port: param.smtp.port,
524            socket: param.smtp.security,
525            username: param.smtp.user.clone(),
526        })
527    }
528
529    let servers = expand_param_vector(servers, &param.addr, &param_domain);
530
531    let configured_login_param = ConfiguredLoginParam {
532        addr,
533        imap: servers
534            .iter()
535            .filter_map(|params| {
536                let Ok(security) = params.socket.try_into() else {
537                    return None;
538                };
539                if params.protocol == Protocol::Imap {
540                    Some(ConfiguredServerLoginParam {
541                        connection: ConnectionCandidate {
542                            host: params.hostname.clone(),
543                            port: params.port,
544                            security,
545                        },
546                        user: params.username.clone(),
547                    })
548                } else {
549                    None
550                }
551            })
552            .collect(),
553        imap_user: param.imap.user.clone(),
554        imap_password: param.imap.password.clone(),
555        smtp: servers
556            .iter()
557            .filter_map(|params| {
558                let Ok(security) = params.socket.try_into() else {
559                    return None;
560                };
561                if params.protocol == Protocol::Smtp {
562                    Some(ConfiguredServerLoginParam {
563                        connection: ConnectionCandidate {
564                            host: params.hostname.clone(),
565                            port: params.port,
566                            security,
567                        },
568                        user: params.username.clone(),
569                    })
570                } else {
571                    None
572                }
573            })
574            .collect(),
575        smtp_user: param.smtp.user.clone(),
576        smtp_password,
577        provider,
578        certificate_checks: match param.certificate_checks {
579            EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
580            EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
581            EnteredCertificateChecks::AcceptInvalidCertificates
582            | EnteredCertificateChecks::AcceptInvalidCertificates2 => {
583                ConfiguredCertificateChecks::AcceptInvalidCertificates
584            }
585        },
586        oauth2: param.oauth2,
587    };
588    Ok(configured_login_param)
589}
590
591async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
592    progress!(ctx, 1);
593
594    let configured_param = get_configured_param(ctx, param).await?;
595    let proxy_config = ProxyConfig::load(ctx).await?;
596    let strict_tls = configured_param.strict_tls(proxy_config.is_some());
597
598    progress!(ctx, 550);
599
600    // Spawn SMTP configuration task
601    // to try SMTP while connecting to IMAP.
602    let context_smtp = ctx.clone();
603    let smtp_param = configured_param.smtp.clone();
604    let smtp_password = configured_param.smtp_password.clone();
605    let smtp_addr = configured_param.addr.clone();
606
607    let proxy_config2 = proxy_config.clone();
608    let smtp_config_task = task::spawn(async move {
609        let mut smtp = Smtp::new();
610        smtp.connect(
611            &context_smtp,
612            &smtp_param,
613            &smtp_password,
614            &proxy_config2,
615            &smtp_addr,
616            strict_tls,
617            configured_param.oauth2,
618        )
619        .await?;
620
621        Ok::<(), anyhow::Error>(())
622    });
623
624    progress!(ctx, 600);
625
626    // Configure IMAP
627
628    let transport_id = 0;
629    let (_s, r) = async_channel::bounded(1);
630    let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
631    let configuring = true;
632    let imap_session = match imap.connect(ctx, configuring).await {
633        Ok(imap_session) => imap_session,
634        Err(err) => {
635            bail!("{}", nicer_configuration_error(ctx, format!("{err:#}")));
636        }
637    };
638
639    progress!(ctx, 850);
640
641    // Wait for SMTP configuration
642    smtp_config_task.await??;
643
644    progress!(ctx, 900);
645
646    let is_configured = ctx.is_configured().await?;
647    if !is_configured {
648        ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
649        ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
650    }
651    if !ctx.get_config_bool(Config::FixIsChatmail).await? {
652        if imap_session.is_chatmail() {
653            ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
654        } else if !is_configured {
655            // Reset the setting that may have been set
656            // during failed configuration.
657            ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
658        }
659    }
660
661    drop(imap_session);
662    drop(imap);
663
664    progress!(ctx, 910);
665
666    let provider = configured_param.provider;
667    configured_param
668        .clone()
669        .save_to_transports_table(ctx, param, time())
670        .await?;
671    send_sync_transports(ctx).await?;
672
673    ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
674        .await?;
675
676    progress!(ctx, 920);
677
678    ctx.scheduler.interrupt_inbox().await;
679
680    progress!(ctx, 940);
681    ctx.update_device_chats()
682        .await
683        .context("Failed to update device chats")?;
684
685    ctx.sql.set_raw_config_bool("configured", true).await?;
686    ctx.emit_event(EventType::AccountsItemChanged);
687
688    Ok(provider)
689}
690
691/// Retrieve available autoconfigurations.
692///
693/// A. Search configurations from the domain used in the email-address
694/// B. If we have no configuration yet, search configuration in Thunderbird's central database
695async fn get_autoconfig(
696    ctx: &Context,
697    param: &EnteredLoginParam,
698    param_domain: &str,
699) -> Option<Vec<ServerParams>> {
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    )
717    .await
718    {
719        return Some(res);
720    }
721    progress!(ctx, 300);
722
723    if let Ok(res) = moz_autoconfigure(
724        ctx,
725        // the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>,  which makes some sense
726        &format!(
727            "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
728            &param_domain, &param_addr_urlencoded
729        ),
730        &param.addr,
731    )
732    .await
733    {
734        return Some(res);
735    }
736    progress!(ctx, 310);
737
738    // Outlook uses always SSL but different domains (this comment describes the next two steps)
739    if let Ok(res) = outlk_autodiscover(
740        ctx,
741        format!("https://{}/autodiscover/autodiscover.xml", &param_domain),
742    )
743    .await
744    {
745        return Some(res);
746    }
747    progress!(ctx, 320);
748
749    if let Ok(res) = outlk_autodiscover(
750        ctx,
751        format!(
752            "https://autodiscover.{}/autodiscover/autodiscover.xml",
753            &param_domain
754        ),
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    )
768    .await
769    {
770        return Some(res);
771    }
772
773    None
774}
775
776fn nicer_configuration_error(context: &Context, e: String) -> String {
777    if e.to_lowercase().contains("could not resolve")
778        || e.to_lowercase().contains("connection attempts")
779        || e.to_lowercase()
780            .contains("temporary failure in name resolution")
781        || e.to_lowercase().contains("name or service not known")
782        || e.to_lowercase()
783            .contains("failed to lookup address information")
784    {
785        return stock_str::error_no_network(context);
786    }
787
788    e
789}
790
791#[derive(Debug, thiserror::Error)]
792pub enum Error {
793    #[error("Invalid email address: {0:?}")]
794    InvalidEmailAddress(String),
795
796    #[error("XML error at position {position}: {error}")]
797    InvalidXml {
798        position: u64,
799        #[source]
800        error: quick_xml::Error,
801    },
802
803    #[error("Number of redirection is exceeded")]
804    Redirection,
805
806    #[error("{0:#}")]
807    Other(#[from] anyhow::Error),
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813    use crate::config::Config;
814    use crate::login_param::EnteredServerLoginParam;
815    use crate::test_utils::TestContext;
816
817    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
818    async fn test_no_panic_on_bad_credentials() {
819        let t = TestContext::new().await;
820        t.set_config(Config::Addr, Some("probably@unexistant.addr"))
821            .await
822            .unwrap();
823        t.set_config(Config::MailPw, Some("123456")).await.unwrap();
824        assert!(t.configure().await.is_err());
825    }
826
827    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
828    async fn test_get_configured_param() -> Result<()> {
829        let t = &TestContext::new().await;
830        let entered_param = EnteredLoginParam {
831            addr: "alice@example.org".to_string(),
832
833            imap: EnteredServerLoginParam {
834                user: "alice@example.net".to_string(),
835                password: "foobar".to_string(),
836                ..Default::default()
837            },
838
839            ..Default::default()
840        };
841        let configured_param = get_configured_param(t, &entered_param).await?;
842        assert_eq!(configured_param.imap_user, "alice@example.net");
843        assert_eq!(configured_param.smtp_user, "");
844        Ok(())
845    }
846}