deltachat/
configure.rs

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