1mod auto_mozilla;
13mod auto_outlook;
14pub(crate) mod server_params;
15
16use anyhow::{bail, ensure, format_err, Context as _, Result};
17use auto_mozilla::moz_autoconfigure;
18use auto_outlook::outlk_autodiscover;
19use deltachat_contact_tools::{addr_normalize, EmailAddress};
20use futures::FutureExt;
21use futures_lite::FutureExt as _;
22use percent_encoding::utf8_percent_encode;
23use server_params::{expand_param_vector, ServerParams};
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;
31pub use crate::login_param::EnteredLoginParam;
32use crate::login_param::{
33 ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
34 ConnectionCandidate, EnteredCertificateChecks, ProxyConfig,
35};
36use crate::message::Message;
37use crate::oauth2::get_oauth2_addr;
38use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
39use crate::smtp::Smtp;
40use crate::sync::Sync::*;
41use crate::tools::time;
42use crate::{chat, provider};
43use crate::{stock_str, EventType};
44use deltachat_contact_tools::addr_cmp;
45
46macro_rules! progress {
47 ($context:tt, $progress:expr, $comment:expr) => {
48 assert!(
49 $progress <= 1000,
50 "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
51 );
52 $context.emit_event($crate::events::EventType::ConfigureProgress {
53 progress: $progress,
54 comment: $comment,
55 });
56 };
57 ($context:tt, $progress:expr) => {
58 progress!($context, $progress, None);
59 };
60}
61
62impl Context {
63 pub async fn is_configured(&self) -> Result<bool> {
65 self.sql.exists("SELECT COUNT(*) FROM transports", ()).await
66 }
67
68 pub async fn configure(&self) -> Result<()> {
73 let mut param = EnteredLoginParam::load(self).await?;
74
75 self.add_transport_inner(&mut param).await
76 }
77
78 pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
108 self.stop_io().await;
109 let result = self.add_transport_inner(param).await;
110 if result.is_err() {
111 if let Ok(true) = self.is_configured().await {
112 self.start_io().await;
113 }
114 return result;
115 }
116 self.start_io().await;
117 Ok(())
118 }
119
120 async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
121 ensure!(
122 !self.scheduler.is_running().await,
123 "cannot configure, already running"
124 );
125 ensure!(
126 self.sql.is_open().await,
127 "cannot configure, database not opened."
128 );
129 param.addr = addr_normalize(¶m.addr);
130 let old_addr = self.get_config(Config::ConfiguredAddr).await?;
131 if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.addr) {
132 let error_msg = "Changing your email address is not supported right now. Check back in a few months!";
133 progress!(self, 0, Some(error_msg.to_string()));
134 bail!(error_msg);
135 }
136 let cancel_channel = self.alloc_ongoing().await?;
137
138 let res = self
139 .inner_configure(param)
140 .race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
141 .await;
142
143 self.free_ongoing().await;
144
145 if let Err(err) = res.as_ref() {
146 let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await;
149 progress!(self, 0, Some(error_msg.clone()));
150 bail!(error_msg);
151 } else {
152 param.save(self).await?;
153 progress!(self, 1000);
154 }
155
156 res
157 }
158
159 pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
163 self.stop_io().await;
164
165 let result = async move {
171 match crate::qr::check_qr(self, qr).await? {
172 crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?,
173 crate::qr::Qr::Login { address, options } => {
174 crate::qr::configure_from_login_qr(self, &address, options).await?
175 }
176 _ => bail!("QR code does not contain account"),
177 }
178 self.configure().await?;
179 Ok(())
180 }
181 .await;
182
183 if result.is_err() {
184 if let Ok(true) = self.is_configured().await {
185 self.start_io().await;
186 }
187 return result;
188 }
189 self.start_io().await;
190 Ok(())
191 }
192
193 pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
197 let transports = self
198 .sql
199 .query_map(
200 "SELECT entered_param FROM transports",
201 (),
202 |row| row.get::<_, String>(0),
203 |rows| {
204 rows.flatten()
205 .map(|s| Ok(serde_json::from_str(&s)?))
206 .collect::<Result<Vec<EnteredLoginParam>>>()
207 },
208 )
209 .await?;
210
211 Ok(transports)
212 }
213
214 #[expect(clippy::unused_async)]
217 pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
218 bail!("Adding and removing additional transports is not supported yet. Check back in a few months!")
219 }
220
221 async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
222 info!(self, "Configure ...");
223
224 let old_addr = self.get_config(Config::ConfiguredAddr).await?;
225 let provider = configure(self, param).await?;
226 self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
227 .await?;
228 on_configure_completed(self, provider, old_addr).await?;
229 Ok(())
230 }
231}
232
233async fn on_configure_completed(
234 context: &Context,
235 provider: Option<&'static Provider>,
236 old_addr: Option<String>,
237) -> Result<()> {
238 if let Some(provider) = provider {
239 if let Some(config_defaults) = provider.config_defaults {
240 for def in config_defaults {
241 if !context.config_exists(def.key).await? {
242 info!(context, "apply config_defaults {}={}", def.key, def.value);
243 context
244 .set_config_ex(Nosync, def.key, Some(def.value))
245 .await?;
246 } else {
247 info!(
248 context,
249 "skip already set config_defaults {}={}", def.key, def.value
250 );
251 }
252 }
253 }
254
255 if !provider.after_login_hint.is_empty() {
256 let mut msg = Message::new_text(provider.after_login_hint.to_string());
257 if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
258 .await
259 .is_err()
260 {
261 warn!(context, "cannot add after_login_hint as core-provider-info");
262 }
263 }
264 }
265
266 if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
267 if let Some(old_addr) = old_addr {
268 if !addr_cmp(&new_addr, &old_addr) {
269 let mut msg = Message::new_text(
270 stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
271 );
272 chat::add_device_msg(context, None, Some(&mut msg))
273 .await
274 .context("Cannot add AEAP explanation")
275 .log_err(context)
276 .ok();
277 }
278 }
279 }
280
281 Ok(())
282}
283
284async fn get_configured_param(
287 ctx: &Context,
288 param: &EnteredLoginParam,
289) -> Result<ConfiguredLoginParam> {
290 ensure!(!param.addr.is_empty(), "Missing email address.");
291
292 ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password.");
293
294 let smtp_password = if param.smtp.password.is_empty() {
296 param.imap.password.clone()
297 } else {
298 param.smtp.password.clone()
299 };
300
301 let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
302
303 let mut addr = param.addr.clone();
304 if param.oauth2 {
305 progress!(ctx, 10);
308 if let Some(oauth2_addr) = get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
309 .await?
310 .and_then(|e| e.parse().ok())
311 {
312 info!(ctx, "Authorized address is {}", oauth2_addr);
313 addr = oauth2_addr;
314 ctx.sql
315 .set_raw_config("addr", Some(param.addr.as_str()))
316 .await?;
317 }
318 progress!(ctx, 20);
319 }
320 let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?;
323 let param_domain = parsed.domain;
324
325 progress!(ctx, 200);
326
327 let provider;
328 let param_autoconfig;
329 if param.imap.server.is_empty()
330 && param.imap.port == 0
331 && param.imap.security == Socket::Automatic
332 && param.imap.user.is_empty()
333 && param.smtp.server.is_empty()
334 && param.smtp.port == 0
335 && param.smtp.security == Socket::Automatic
336 && param.smtp.user.is_empty()
337 {
338 info!(
340 ctx,
341 "checking internal provider-info for offline autoconfig"
342 );
343
344 provider = provider::get_provider_info(ctx, ¶m_domain, proxy_enabled).await;
345 if let Some(provider) = provider {
346 if provider.server.is_empty() {
347 info!(ctx, "Offline autoconfig found, but no servers defined.");
348 param_autoconfig = None;
349 } else {
350 info!(ctx, "Offline autoconfig found.");
351 let servers = provider
352 .server
353 .iter()
354 .map(|s| ServerParams {
355 protocol: s.protocol,
356 socket: s.socket,
357 hostname: s.hostname.to_string(),
358 port: s.port,
359 username: match s.username_pattern {
360 UsernamePattern::Email => param.addr.to_string(),
361 UsernamePattern::Emaillocalpart => {
362 if let Some(at) = param.addr.find('@') {
363 param.addr.split_at(at).0.to_string()
364 } else {
365 param.addr.to_string()
366 }
367 }
368 },
369 })
370 .collect();
371
372 param_autoconfig = Some(servers)
373 }
374 } else {
375 info!(ctx, "No offline autoconfig found.");
377 param_autoconfig = get_autoconfig(ctx, param, ¶m_domain).await;
378 }
379 } else {
380 provider = None;
381 param_autoconfig = None;
382 }
383
384 progress!(ctx, 500);
385
386 let mut servers = param_autoconfig.unwrap_or_default();
387 if !servers
388 .iter()
389 .any(|server| server.protocol == Protocol::Imap)
390 {
391 servers.push(ServerParams {
392 protocol: Protocol::Imap,
393 hostname: param.imap.server.clone(),
394 port: param.imap.port,
395 socket: param.imap.security,
396 username: param.imap.user.clone(),
397 })
398 }
399 if !servers
400 .iter()
401 .any(|server| server.protocol == Protocol::Smtp)
402 {
403 servers.push(ServerParams {
404 protocol: Protocol::Smtp,
405 hostname: param.smtp.server.clone(),
406 port: param.smtp.port,
407 socket: param.smtp.security,
408 username: param.smtp.user.clone(),
409 })
410 }
411
412 let servers = expand_param_vector(servers, ¶m.addr, ¶m_domain);
413
414 let configured_login_param = ConfiguredLoginParam {
415 addr,
416 imap: servers
417 .iter()
418 .filter_map(|params| {
419 let Ok(security) = params.socket.try_into() else {
420 return None;
421 };
422 if params.protocol == Protocol::Imap {
423 Some(ConfiguredServerLoginParam {
424 connection: ConnectionCandidate {
425 host: params.hostname.clone(),
426 port: params.port,
427 security,
428 },
429 user: params.username.clone(),
430 })
431 } else {
432 None
433 }
434 })
435 .collect(),
436 imap_user: param.imap.user.clone(),
437 imap_password: param.imap.password.clone(),
438 smtp: servers
439 .iter()
440 .filter_map(|params| {
441 let Ok(security) = params.socket.try_into() else {
442 return None;
443 };
444 if params.protocol == Protocol::Smtp {
445 Some(ConfiguredServerLoginParam {
446 connection: ConnectionCandidate {
447 host: params.hostname.clone(),
448 port: params.port,
449 security,
450 },
451 user: params.username.clone(),
452 })
453 } else {
454 None
455 }
456 })
457 .collect(),
458 smtp_user: param.smtp.user.clone(),
459 smtp_password,
460 provider,
461 certificate_checks: match param.certificate_checks {
462 EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
463 EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
464 EnteredCertificateChecks::AcceptInvalidCertificates
465 | EnteredCertificateChecks::AcceptInvalidCertificates2 => {
466 ConfiguredCertificateChecks::AcceptInvalidCertificates
467 }
468 },
469 oauth2: param.oauth2,
470 };
471 Ok(configured_login_param)
472}
473
474async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
475 progress!(ctx, 1);
476
477 let ctx2 = ctx.clone();
478 let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
479
480 let configured_param = get_configured_param(ctx, param).await?;
481 let proxy_config = ProxyConfig::load(ctx).await?;
482 let strict_tls = configured_param.strict_tls(proxy_config.is_some());
483
484 progress!(ctx, 550);
485
486 let context_smtp = ctx.clone();
489 let smtp_param = configured_param.smtp.clone();
490 let smtp_password = configured_param.smtp_password.clone();
491 let smtp_addr = configured_param.addr.clone();
492
493 let proxy_config2 = proxy_config.clone();
494 let smtp_config_task = task::spawn(async move {
495 let mut smtp = Smtp::new();
496 smtp.connect(
497 &context_smtp,
498 &smtp_param,
499 &smtp_password,
500 &proxy_config2,
501 &smtp_addr,
502 strict_tls,
503 configured_param.oauth2,
504 )
505 .await?;
506
507 Ok::<(), anyhow::Error>(())
508 });
509
510 progress!(ctx, 600);
511
512 let (_s, r) = async_channel::bounded(1);
515 let mut imap = Imap::new(
516 configured_param.imap.clone(),
517 configured_param.imap_password.clone(),
518 proxy_config,
519 &configured_param.addr,
520 strict_tls,
521 configured_param.oauth2,
522 r,
523 );
524 let configuring = true;
525 let mut imap_session = match imap.connect(ctx, configuring).await {
526 Ok(session) => session,
527 Err(err) => bail!(
528 "{}",
529 nicer_configuration_error(ctx, format!("{err:#}")).await
530 ),
531 };
532
533 progress!(ctx, 850);
534
535 smtp_config_task.await.unwrap()?;
537
538 progress!(ctx, 900);
539
540 let is_chatmail = match ctx.get_config_bool(Config::FixIsChatmail).await? {
541 false => {
542 let is_chatmail = imap_session.is_chatmail();
543 ctx.set_config(
544 Config::IsChatmail,
545 Some(match is_chatmail {
546 false => "0",
547 true => "1",
548 }),
549 )
550 .await?;
551 is_chatmail
552 }
553 true => ctx.get_config_bool(Config::IsChatmail).await?,
554 };
555 if is_chatmail {
556 ctx.set_config(Config::SentboxWatch, None).await?;
557 ctx.set_config(Config::MvboxMove, Some("0")).await?;
558 ctx.set_config(Config::OnlyFetchMvbox, None).await?;
559 ctx.set_config(Config::ShowEmails, None).await?;
560 }
561
562 let create_mvbox = !is_chatmail;
563 imap.configure_folders(ctx, &mut imap_session, create_mvbox)
564 .await?;
565
566 let create = true;
567 imap_session
568 .select_with_uidvalidity(ctx, "INBOX", create)
569 .await
570 .context("could not read INBOX status")?;
571
572 drop(imap);
573
574 progress!(ctx, 910);
575
576 if let Some(configured_addr) = ctx.get_config(Config::ConfiguredAddr).await? {
577 if configured_addr != param.addr {
578 info!(ctx, "Scheduling resync because the address has changed.");
580 ctx.schedule_resync().await?;
581 }
582 }
583
584 let provider = configured_param.provider;
585 configured_param
586 .save_to_transports_table(ctx, param)
587 .await?;
588
589 ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
590 .await?;
591
592 progress!(ctx, 920);
593
594 ctx.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(false))
595 .await?;
596 ctx.scheduler.interrupt_inbox().await;
597
598 progress!(ctx, 940);
599 update_device_chats_handle.await??;
600
601 ctx.sql.set_raw_config_bool("configured", true).await?;
602 ctx.emit_event(EventType::AccountsItemChanged);
603
604 Ok(provider)
605}
606
607async fn get_autoconfig(
612 ctx: &Context,
613 param: &EnteredLoginParam,
614 param_domain: &str,
615) -> Option<Vec<ServerParams>> {
616 let param_addr_urlencoded =
624 utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
625
626 if let Ok(res) = moz_autoconfigure(
627 ctx,
628 &format!(
629 "https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
630 ),
631 ¶m.addr,
632 )
633 .await
634 {
635 return Some(res);
636 }
637 progress!(ctx, 300);
638
639 if let Ok(res) = moz_autoconfigure(
640 ctx,
641 &format!(
643 "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
644 ¶m_domain, ¶m_addr_urlencoded
645 ),
646 ¶m.addr,
647 )
648 .await
649 {
650 return Some(res);
651 }
652 progress!(ctx, 310);
653
654 if let Ok(res) = outlk_autodiscover(
656 ctx,
657 format!("https://{}/autodiscover/autodiscover.xml", ¶m_domain),
658 )
659 .await
660 {
661 return Some(res);
662 }
663 progress!(ctx, 320);
664
665 if let Ok(res) = outlk_autodiscover(
666 ctx,
667 format!(
668 "https://autodiscover.{}/autodiscover/autodiscover.xml",
669 ¶m_domain
670 ),
671 )
672 .await
673 {
674 return Some(res);
675 }
676 progress!(ctx, 330);
677
678 if let Ok(res) = moz_autoconfigure(
680 ctx,
681 &format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
682 ¶m.addr,
683 )
684 .await
685 {
686 return Some(res);
687 }
688
689 None
690}
691
692async fn nicer_configuration_error(context: &Context, e: String) -> String {
693 if e.to_lowercase().contains("could not resolve")
694 || e.to_lowercase().contains("connection attempts")
695 || e.to_lowercase()
696 .contains("temporary failure in name resolution")
697 || e.to_lowercase().contains("name or service not known")
698 || e.to_lowercase()
699 .contains("failed to lookup address information")
700 {
701 return stock_str::error_no_network(context).await;
702 }
703
704 e
705}
706
707#[derive(Debug, thiserror::Error)]
708pub enum Error {
709 #[error("Invalid email address: {0:?}")]
710 InvalidEmailAddress(String),
711
712 #[error("XML error at position {position}: {error}")]
713 InvalidXml {
714 position: u64,
715 #[source]
716 error: quick_xml::Error,
717 },
718
719 #[error("Number of redirection is exceeded")]
720 Redirection,
721
722 #[error("{0:#}")]
723 Other(#[from] anyhow::Error),
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use crate::config::Config;
730 use crate::login_param::EnteredServerLoginParam;
731 use crate::test_utils::TestContext;
732
733 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
734 async fn test_no_panic_on_bad_credentials() {
735 let t = TestContext::new().await;
736 t.set_config(Config::Addr, Some("probably@unexistant.addr"))
737 .await
738 .unwrap();
739 t.set_config(Config::MailPw, Some("123456")).await.unwrap();
740 assert!(t.configure().await.is_err());
741 }
742
743 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
744 async fn test_get_configured_param() -> Result<()> {
745 let t = &TestContext::new().await;
746 let entered_param = EnteredLoginParam {
747 addr: "alice@example.org".to_string(),
748
749 imap: EnteredServerLoginParam {
750 user: "alice@example.net".to_string(),
751 password: "foobar".to_string(),
752 ..Default::default()
753 },
754
755 ..Default::default()
756 };
757 let configured_param = get_configured_param(t, &entered_param).await?;
758 assert_eq!(configured_param.imap_user, "alice@example.net");
759 assert_eq!(configured_param.smtp_user, "");
760 Ok(())
761 }
762}