1mod 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;
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, send_sync_transports,
44};
45use crate::{EventType, stock_str};
46use crate::{chat, provider};
47
48pub(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 pub async fn is_configured(&self) -> Result<bool> {
71 self.sql.exists("SELECT COUNT(*) FROM transports", ()).await
72 }
73
74 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 pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
114 self.stop_io().await;
115 let result = self.add_transport_inner(param).await;
116 if result.is_err() {
117 if let Ok(true) = self.is_configured().await {
118 self.start_io().await;
119 }
120 return result;
121 }
122 self.start_io().await;
123 Ok(())
124 }
125
126 pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
127 ensure!(
128 !self.scheduler.is_running().await,
129 "cannot configure, already running"
130 );
131 ensure!(
132 self.sql.is_open().await,
133 "cannot configure, database not opened."
134 );
135 param.addr = addr_normalize(¶m.addr);
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!("Canceled"))))
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 {
166 let mut param = match crate::qr::check_qr(self, qr).await? {
167 crate::qr::Qr::Account { .. } => login_param_from_account_qr(self, qr).await?,
168 crate::qr::Qr::Login { address, options } => {
169 login_param_from_login_qr(&address, options)?
170 }
171 _ => bail!("QR code does not contain account"),
172 };
173 self.add_transport_inner(&mut param).await?;
174 Ok(())
175 }
176 .await;
177
178 if result.is_err() {
179 if let Ok(true) = self.is_configured().await {
180 self.start_io().await;
181 }
182 return result;
183 }
184 self.start_io().await;
185 Ok(())
186 }
187
188 pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
192 let transports = self
193 .sql
194 .query_map_vec("SELECT entered_param FROM transports", (), |row| {
195 let entered_param: String = row.get(0)?;
196 let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?;
197 Ok(transport)
198 })
199 .await?;
200
201 Ok(transports)
202 }
203
204 pub async fn count_transports(&self) -> Result<usize> {
206 self.sql.count("SELECT COUNT(*) FROM transports", ()).await
207 }
208
209 pub async fn delete_transport(&self, addr: &str) -> Result<()> {
212 let now = time();
213 let removed_transport_id = self
214 .sql
215 .transaction(|transaction| {
216 let primary_addr = transaction.query_row(
217 "SELECT value FROM config WHERE keyname='configured_addr'",
218 (),
219 |row| {
220 let addr: String = row.get(0)?;
221 Ok(addr)
222 },
223 )?;
224
225 if primary_addr == addr {
226 bail!("Cannot delete primary transport");
227 }
228 let (transport_id, add_timestamp) = transaction.query_row(
229 "DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp",
230 (addr,),
231 |row| {
232 let id: u32 = row.get(0)?;
233 let add_timestamp: i64 = row.get(1)?;
234 Ok((id, add_timestamp))
235 },
236 )?;
237 transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
238 transaction.execute(
239 "DELETE FROM imap_sync WHERE transport_id=?",
240 (transport_id,),
241 )?;
242
243 let remove_timestamp = std::cmp::max(now, add_timestamp);
246
247 transaction.execute(
248 "INSERT INTO removed_transports (addr, remove_timestamp)
249 VALUES (?, ?)
250 ON CONFLICT (addr)
251 DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
252 (addr, remove_timestamp),
253 )?;
254
255 Ok(transport_id)
256 })
257 .await?;
258 send_sync_transports(self).await?;
259 self.quota.write().await.remove(&removed_transport_id);
260
261 Ok(())
262 }
263
264 async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
265 info!(self, "Configure ...");
266
267 let old_addr = self.get_config(Config::ConfiguredAddr).await?;
268 if old_addr.is_some()
269 && !self
270 .sql
271 .exists(
272 "SELECT COUNT(*) FROM transports WHERE addr=?",
273 (¶m.addr,),
274 )
275 .await?
276 {
277 if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
280 bail!(
281 "To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"."
282 );
283 }
284 if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
285 bail!(
286 "To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"."
287 );
288 }
289
290 if self
291 .sql
292 .count("SELECT COUNT(*) FROM transports", ())
293 .await?
294 >= MAX_TRANSPORT_RELAYS
295 {
296 bail!(
297 "You have reached the maximum number of relays ({}).",
298 MAX_TRANSPORT_RELAYS
299 )
300 }
301 }
302
303 let provider = match configure(self, param).await {
304 Err(error) => {
305 let configured_param = get_configured_param(self, param).await;
307 warn!(
308 self,
309 "configure failed: Entered params: {}. Used params: {}. Error: {error}.",
310 param.to_string(),
311 configured_param
312 .map(|param| param.to_string())
313 .unwrap_or("error".to_owned())
314 );
315 return Err(error);
316 }
317 Ok(provider) => provider,
318 };
319 self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
320 .await?;
321 on_configure_completed(self, provider).await?;
322 Ok(())
323 }
324}
325
326async fn on_configure_completed(
327 context: &Context,
328 provider: Option<&'static Provider>,
329) -> Result<()> {
330 if let Some(provider) = provider {
331 if let Some(config_defaults) = provider.config_defaults {
332 for def in config_defaults {
333 if !context.config_exists(def.key).await? {
334 info!(context, "apply config_defaults {}={}", def.key, def.value);
335 context
336 .set_config_ex(Nosync, def.key, Some(def.value))
337 .await?;
338 } else {
339 info!(
340 context,
341 "skip already set config_defaults {}={}", def.key, def.value
342 );
343 }
344 }
345 }
346
347 if !provider.after_login_hint.is_empty() {
348 let mut msg = Message::new_text(provider.after_login_hint.to_string());
349 if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
350 .await
351 .is_err()
352 {
353 warn!(context, "cannot add after_login_hint as core-provider-info");
354 }
355 }
356 }
357
358 Ok(())
359}
360
361async fn get_configured_param(
364 ctx: &Context,
365 param: &EnteredLoginParam,
366) -> Result<ConfiguredLoginParam> {
367 ensure!(!param.addr.is_empty(), "Missing email address.");
368
369 ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password.");
370
371 let smtp_password = if param.smtp.password.is_empty() {
373 param.imap.password.clone()
374 } else {
375 param.smtp.password.clone()
376 };
377
378 let mut addr = param.addr.clone();
379 if param.oauth2 {
380 progress!(ctx, 10);
383 if let Some(oauth2_addr) = get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
384 .await?
385 .and_then(|e| e.parse().ok())
386 {
387 info!(ctx, "Authorized address is {}", oauth2_addr);
388 addr = oauth2_addr;
389 ctx.sql
390 .set_raw_config("addr", Some(param.addr.as_str()))
391 .await?;
392 }
393 progress!(ctx, 20);
394 }
395 let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?;
398 let param_domain = parsed.domain;
399
400 progress!(ctx, 200);
401
402 let provider;
403 let param_autoconfig;
404 if param.imap.server.is_empty()
405 && param.imap.port == 0
406 && param.imap.security == Socket::Automatic
407 && param.imap.user.is_empty()
408 && param.smtp.server.is_empty()
409 && param.smtp.port == 0
410 && param.smtp.security == Socket::Automatic
411 && param.smtp.user.is_empty()
412 {
413 info!(
415 ctx,
416 "checking internal provider-info for offline autoconfig"
417 );
418
419 provider = provider::get_provider_info(¶m_domain);
420 if let Some(provider) = provider {
421 if provider.server.is_empty() {
422 info!(ctx, "Offline autoconfig found, but no servers defined.");
423 param_autoconfig = None;
424 } else {
425 info!(ctx, "Offline autoconfig found.");
426 let servers = provider
427 .server
428 .iter()
429 .map(|s| ServerParams {
430 protocol: s.protocol,
431 socket: s.socket,
432 hostname: s.hostname.to_string(),
433 port: s.port,
434 username: match s.username_pattern {
435 UsernamePattern::Email => param.addr.to_string(),
436 UsernamePattern::Emaillocalpart => {
437 if let Some(at) = param.addr.find('@') {
438 param.addr.split_at(at).0.to_string()
439 } else {
440 param.addr.to_string()
441 }
442 }
443 },
444 })
445 .collect();
446
447 param_autoconfig = Some(servers)
448 }
449 } else {
450 info!(ctx, "No offline autoconfig found.");
452 param_autoconfig = get_autoconfig(ctx, param, ¶m_domain).await;
453 }
454 } else {
455 provider = None;
456 param_autoconfig = None;
457 }
458
459 progress!(ctx, 500);
460
461 let mut servers = param_autoconfig.unwrap_or_default();
462 if !servers
463 .iter()
464 .any(|server| server.protocol == Protocol::Imap)
465 {
466 servers.push(ServerParams {
467 protocol: Protocol::Imap,
468 hostname: param.imap.server.clone(),
469 port: param.imap.port,
470 socket: param.imap.security,
471 username: param.imap.user.clone(),
472 })
473 }
474 if !servers
475 .iter()
476 .any(|server| server.protocol == Protocol::Smtp)
477 {
478 servers.push(ServerParams {
479 protocol: Protocol::Smtp,
480 hostname: param.smtp.server.clone(),
481 port: param.smtp.port,
482 socket: param.smtp.security,
483 username: param.smtp.user.clone(),
484 })
485 }
486
487 let servers = expand_param_vector(servers, ¶m.addr, ¶m_domain);
488
489 let configured_login_param = ConfiguredLoginParam {
490 addr,
491 imap: servers
492 .iter()
493 .filter_map(|params| {
494 let Ok(security) = params.socket.try_into() else {
495 return None;
496 };
497 if params.protocol == Protocol::Imap {
498 Some(ConfiguredServerLoginParam {
499 connection: ConnectionCandidate {
500 host: params.hostname.clone(),
501 port: params.port,
502 security,
503 },
504 user: params.username.clone(),
505 })
506 } else {
507 None
508 }
509 })
510 .collect(),
511 imap_user: param.imap.user.clone(),
512 imap_password: param.imap.password.clone(),
513 smtp: servers
514 .iter()
515 .filter_map(|params| {
516 let Ok(security) = params.socket.try_into() else {
517 return None;
518 };
519 if params.protocol == Protocol::Smtp {
520 Some(ConfiguredServerLoginParam {
521 connection: ConnectionCandidate {
522 host: params.hostname.clone(),
523 port: params.port,
524 security,
525 },
526 user: params.username.clone(),
527 })
528 } else {
529 None
530 }
531 })
532 .collect(),
533 smtp_user: param.smtp.user.clone(),
534 smtp_password,
535 provider,
536 certificate_checks: match param.certificate_checks {
537 EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
538 EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
539 EnteredCertificateChecks::AcceptInvalidCertificates
540 | EnteredCertificateChecks::AcceptInvalidCertificates2 => {
541 ConfiguredCertificateChecks::AcceptInvalidCertificates
542 }
543 },
544 oauth2: param.oauth2,
545 };
546 Ok(configured_login_param)
547}
548
549async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
550 progress!(ctx, 1);
551
552 let configured_param = get_configured_param(ctx, param).await?;
553 let proxy_config = ProxyConfig::load(ctx).await?;
554 let strict_tls = configured_param.strict_tls(proxy_config.is_some());
555
556 progress!(ctx, 550);
557
558 let context_smtp = ctx.clone();
561 let smtp_param = configured_param.smtp.clone();
562 let smtp_password = configured_param.smtp_password.clone();
563 let smtp_addr = configured_param.addr.clone();
564
565 let proxy_config2 = proxy_config.clone();
566 let smtp_config_task = task::spawn(async move {
567 let mut smtp = Smtp::new();
568 smtp.connect(
569 &context_smtp,
570 &smtp_param,
571 &smtp_password,
572 &proxy_config2,
573 &smtp_addr,
574 strict_tls,
575 configured_param.oauth2,
576 )
577 .await?;
578
579 Ok::<(), anyhow::Error>(())
580 });
581
582 progress!(ctx, 600);
583
584 let transport_id = 0;
587 let (_s, r) = async_channel::bounded(1);
588 let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
589 let configuring = true;
590 let imap_session = match imap.connect(ctx, configuring).await {
591 Ok(imap_session) => imap_session,
592 Err(err) => {
593 bail!(
594 "{}",
595 nicer_configuration_error(ctx, format!("{err:#}")).await
596 );
597 }
598 };
599
600 progress!(ctx, 850);
601
602 smtp_config_task.await??;
604
605 progress!(ctx, 900);
606
607 let is_configured = ctx.is_configured().await?;
608 if !is_configured {
609 ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
610 ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
611 }
612 if !ctx.get_config_bool(Config::FixIsChatmail).await? {
613 if imap_session.is_chatmail() {
614 ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
615 } else if !is_configured {
616 ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
619 }
620 }
621
622 drop(imap_session);
623 drop(imap);
624
625 progress!(ctx, 910);
626
627 let provider = configured_param.provider;
628 configured_param
629 .clone()
630 .save_to_transports_table(ctx, param, time())
631 .await?;
632 send_sync_transports(ctx).await?;
633
634 ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
635 .await?;
636
637 progress!(ctx, 920);
638
639 ctx.scheduler.interrupt_inbox().await;
640
641 progress!(ctx, 940);
642 ctx.update_device_chats()
643 .await
644 .context("Failed to update device chats")?;
645
646 ctx.sql.set_raw_config_bool("configured", true).await?;
647 ctx.emit_event(EventType::AccountsItemChanged);
648
649 Ok(provider)
650}
651
652async fn get_autoconfig(
657 ctx: &Context,
658 param: &EnteredLoginParam,
659 param_domain: &str,
660) -> Option<Vec<ServerParams>> {
661 let param_addr_urlencoded =
669 utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
670
671 if let Ok(res) = moz_autoconfigure(
672 ctx,
673 &format!(
674 "https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
675 ),
676 ¶m.addr,
677 )
678 .await
679 {
680 return Some(res);
681 }
682 progress!(ctx, 300);
683
684 if let Ok(res) = moz_autoconfigure(
685 ctx,
686 &format!(
688 "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
689 ¶m_domain, ¶m_addr_urlencoded
690 ),
691 ¶m.addr,
692 )
693 .await
694 {
695 return Some(res);
696 }
697 progress!(ctx, 310);
698
699 if let Ok(res) = outlk_autodiscover(
701 ctx,
702 format!("https://{}/autodiscover/autodiscover.xml", ¶m_domain),
703 )
704 .await
705 {
706 return Some(res);
707 }
708 progress!(ctx, 320);
709
710 if let Ok(res) = outlk_autodiscover(
711 ctx,
712 format!(
713 "https://autodiscover.{}/autodiscover/autodiscover.xml",
714 ¶m_domain
715 ),
716 )
717 .await
718 {
719 return Some(res);
720 }
721 progress!(ctx, 330);
722
723 if let Ok(res) = moz_autoconfigure(
725 ctx,
726 &format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
727 ¶m.addr,
728 )
729 .await
730 {
731 return Some(res);
732 }
733
734 None
735}
736
737async fn nicer_configuration_error(context: &Context, e: String) -> String {
738 if e.to_lowercase().contains("could not resolve")
739 || e.to_lowercase().contains("connection attempts")
740 || e.to_lowercase()
741 .contains("temporary failure in name resolution")
742 || e.to_lowercase().contains("name or service not known")
743 || e.to_lowercase()
744 .contains("failed to lookup address information")
745 {
746 return stock_str::error_no_network(context).await;
747 }
748
749 e
750}
751
752#[derive(Debug, thiserror::Error)]
753pub enum Error {
754 #[error("Invalid email address: {0:?}")]
755 InvalidEmailAddress(String),
756
757 #[error("XML error at position {position}: {error}")]
758 InvalidXml {
759 position: u64,
760 #[source]
761 error: quick_xml::Error,
762 },
763
764 #[error("Number of redirection is exceeded")]
765 Redirection,
766
767 #[error("{0:#}")]
768 Other(#[from] anyhow::Error),
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774 use crate::config::Config;
775 use crate::login_param::EnteredServerLoginParam;
776 use crate::test_utils::TestContext;
777
778 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
779 async fn test_no_panic_on_bad_credentials() {
780 let t = TestContext::new().await;
781 t.set_config(Config::Addr, Some("probably@unexistant.addr"))
782 .await
783 .unwrap();
784 t.set_config(Config::MailPw, Some("123456")).await.unwrap();
785 assert!(t.configure().await.is_err());
786 }
787
788 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
789 async fn test_get_configured_param() -> Result<()> {
790 let t = &TestContext::new().await;
791 let entered_param = EnteredLoginParam {
792 addr: "alice@example.org".to_string(),
793
794 imap: EnteredServerLoginParam {
795 user: "alice@example.net".to_string(),
796 password: "foobar".to_string(),
797 ..Default::default()
798 },
799
800 ..Default::default()
801 };
802 let configured_param = get_configured_param(t, &entered_param).await?;
803 assert_eq!(configured_param.imap_user, "alice@example.net");
804 assert_eq!(configured_param.smtp_user, "");
805 Ok(())
806 }
807}