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;
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
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<()> {
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(¶m.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 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 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 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(¶m)?;
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 pub async fn count_transports(&self) -> Result<usize> {
215 self.sql.count("SELECT COUNT(*) FROM transports", ()).await
216 }
217
218 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 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 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 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 (¶m.addr,),
316 )
317 .await?
318 {
319 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 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
403async 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 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 progress!(ctx, 10);
425 if let Some(oauth2_addr) = get_oauth2_addr(ctx, ¶m.addr, ¶m.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 let parsed = EmailAddress::new(¶m.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 info!(
457 ctx,
458 "checking internal provider-info for offline autoconfig"
459 );
460
461 provider = provider::get_provider_info(¶m_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 info!(ctx, "No offline autoconfig found.");
494 param_autoconfig = get_autoconfig(ctx, param, ¶m_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, ¶m.addr, ¶m_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 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 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 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 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
691async fn get_autoconfig(
696 ctx: &Context,
697 param: &EnteredLoginParam,
698 param_domain: &str,
699) -> Option<Vec<ServerParams>> {
700 let param_addr_urlencoded =
708 utf8_percent_encode(¶m.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 ¶m.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 &format!(
727 "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
728 ¶m_domain, ¶m_addr_urlencoded
729 ),
730 ¶m.addr,
731 )
732 .await
733 {
734 return Some(res);
735 }
736 progress!(ctx, 310);
737
738 if let Ok(res) = outlk_autodiscover(
740 ctx,
741 format!("https://{}/autodiscover/autodiscover.xml", ¶m_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 ¶m_domain
754 ),
755 )
756 .await
757 {
758 return Some(res);
759 }
760 progress!(ctx, 330);
761
762 if let Ok(res) = moz_autoconfigure(
764 ctx,
765 &format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
766 ¶m.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}