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
52#[allow(unused)]
58const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
59 "mehl.cloud",
60 "mailchat.pl",
61 "chatmail.woodpeckersnest.space",
62 "chatmail.culturanerd.it",
63 "tarpit.fun",
64 "d.gaufr.es",
65];
66
67macro_rules! progress {
68 ($context:tt, $progress:expr, $comment:expr) => {
69 assert!(
70 $progress <= 1000,
71 "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
72 );
73 $context.emit_event($crate::events::EventType::ConfigureProgress {
74 progress: $progress,
75 comment: $comment,
76 });
77 };
78 ($context:tt, $progress:expr) => {
79 progress!($context, $progress, None);
80 };
81}
82
83impl Context {
84 pub async fn is_configured(&self) -> Result<bool> {
86 self.sql.exists("SELECT COUNT(*) FROM transports", ()).await
87 }
88
89 pub async fn configure(&self) -> Result<()> {
94 let mut param = EnteredLoginParam::load_legacy(self).await?;
95
96 self.add_transport_inner(&mut param).await
97 }
98
99 pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
130 self.stop_io().await;
131 let result = self.add_transport_inner(param).await;
132 if result.is_err() {
133 if let Ok(true) = self.is_configured().await {
134 self.start_io().await;
135 }
136 return result;
137 }
138 self.start_io().await;
139 Ok(())
140 }
141
142 pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
143 ensure!(
144 !self.scheduler.is_running().await,
145 "cannot configure, already running"
146 );
147 ensure!(
148 self.sql.is_open().await,
149 "cannot configure, database not opened."
150 );
151 param.addr = addr_normalize(¶m.addr);
152 let cancel_channel = self.alloc_ongoing().await?;
153
154 let res = self
155 .inner_configure(param)
156 .race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
157 .await;
158
159 self.free_ongoing().await;
160
161 if let Err(err) = res.as_ref() {
162 let error_msg = stock_str::configuration_failed(self, &format!("{err:#}"));
165 progress!(self, 0, Some(error_msg.clone()));
166 bail!(error_msg);
167 } else {
168 param.save_legacy(self).await?;
169 progress!(self, 1000);
170 }
171
172 res
173 }
174
175 pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
179 self.stop_io().await;
180
181 let result = async move {
182 let mut param = match crate::qr::check_qr(self, qr).await? {
183 crate::qr::Qr::Account { .. } => login_param_from_account_qr(self, qr).await?,
184 crate::qr::Qr::Login { address, options } => {
185 login_param_from_login_qr(&address, options)?
186 }
187 _ => bail!("QR code does not contain account"),
188 };
189 self.add_transport_inner(&mut param).await?;
190 Ok(())
191 }
192 .await;
193
194 if result.is_err() {
195 if let Ok(true) = self.is_configured().await {
196 self.start_io().await;
197 }
198 return result;
199 }
200 self.start_io().await;
201 Ok(())
202 }
203
204 pub async fn list_transports(&self) -> Result<Vec<TransportListEntry>> {
208 let transports = self
209 .sql
210 .query_map_vec(
211 "SELECT entered_param, is_published FROM transports",
212 (),
213 |row| {
214 let param: String = row.get(0)?;
215 let param: EnteredLoginParam = serde_json::from_str(¶m)?;
216 let is_published: bool = row.get(1)?;
217 Ok(TransportListEntry {
218 param,
219 is_unpublished: !is_published,
220 })
221 },
222 )
223 .await?;
224
225 Ok(transports)
226 }
227
228 pub async fn count_transports(&self) -> Result<usize> {
230 self.sql.count("SELECT COUNT(*) FROM transports", ()).await
231 }
232
233 pub async fn delete_transport(&self, addr: &str) -> Result<()> {
236 let now = time();
237 let removed_transport_id = self
238 .sql
239 .transaction(|transaction| {
240 let primary_addr = transaction.query_row(
241 "SELECT value FROM config WHERE keyname='configured_addr'",
242 (),
243 |row| {
244 let addr: String = row.get(0)?;
245 Ok(addr)
246 },
247 )?;
248
249 if primary_addr == addr {
250 bail!("Cannot delete primary transport");
251 }
252 let (transport_id, add_timestamp) = transaction.query_row(
253 "DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp",
254 (addr,),
255 |row| {
256 let id: u32 = row.get(0)?;
257 let add_timestamp: i64 = row.get(1)?;
258 Ok((id, add_timestamp))
259 },
260 )?;
261
262 let remove_timestamp = std::cmp::max(now, add_timestamp);
265
266 transaction.execute(
267 "INSERT INTO removed_transports (addr, remove_timestamp)
268 VALUES (?, ?)
269 ON CONFLICT (addr)
270 DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
271 (addr, remove_timestamp),
272 )?;
273
274 Ok(transport_id)
275 })
276 .await?;
277 send_sync_transports(self).await?;
278 self.quota.write().await.remove(&removed_transport_id);
279 self.restart_io_if_running().await;
280
281 Ok(())
282 }
283
284 pub async fn set_transport_unpublished(&self, addr: &str, unpublished: bool) -> Result<()> {
295 self.sql
296 .transaction(|trans| {
297 let primary_addr: String = trans
298 .query_row(
299 "SELECT value FROM config WHERE keyname='configured_addr'",
300 (),
301 |row| row.get(0),
302 )
303 .context("Select primary address")?;
304 if primary_addr == addr && unpublished {
305 bail!("Can't set primary relay as unpublished");
306 }
307 trans
310 .execute(
311 "UPDATE transports SET is_published=?, add_timestamp=? WHERE addr=? AND is_published!=?1",
312 (!unpublished, time(), addr),
313 )
314 .context("Update transports")?;
315 Ok(())
316 })
317 .await?;
318 send_sync_transports(self).await?;
319 Ok(())
320 }
321
322 async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
323 info!(self, "Configure ...");
324
325 let old_addr = self.get_config(Config::ConfiguredAddr).await?;
326 if old_addr.is_some()
327 && !self
328 .sql
329 .exists(
330 "SELECT COUNT(*) FROM transports WHERE addr=?",
331 (¶m.addr,),
332 )
333 .await?
334 && self
335 .sql
336 .count("SELECT COUNT(*) FROM transports", ())
337 .await?
338 >= MAX_TRANSPORT_RELAYS
339 {
340 bail!(
341 "You have reached the maximum number of relays ({}).",
342 MAX_TRANSPORT_RELAYS
343 )
344 }
345
346 let provider = match configure(self, param).await {
347 Err(error) => {
348 let configured_param = get_configured_param(self, param).await;
350 warn!(
351 self,
352 "configure failed: Entered params: {}. Used params: {}. Error: {error}.",
353 param.to_string(),
354 configured_param
355 .map(|param| param.to_string())
356 .unwrap_or("error".to_owned())
357 );
358 return Err(error);
359 }
360 Ok(provider) => provider,
361 };
362 self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
363 .await?;
364 on_configure_completed(self, provider).await?;
365 Ok(())
366 }
367}
368
369async fn on_configure_completed(
370 context: &Context,
371 provider: Option<&'static Provider>,
372) -> Result<()> {
373 if let Some(provider) = provider {
374 if let Some(config_defaults) = provider.config_defaults {
375 for def in config_defaults {
376 if !context.config_exists(def.key).await? {
377 info!(context, "apply config_defaults {}={}", def.key, def.value);
378 context
379 .set_config_ex(Nosync, def.key, Some(def.value))
380 .await?;
381 } else {
382 info!(
383 context,
384 "skip already set config_defaults {}={}", def.key, def.value
385 );
386 }
387 }
388 }
389
390 if !provider.after_login_hint.is_empty() {
391 let mut msg = Message::new_text(provider.after_login_hint.to_string());
392 if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
393 .await
394 .is_err()
395 {
396 warn!(context, "cannot add after_login_hint as core-provider-info");
397 }
398 }
399 }
400
401 Ok(())
402}
403
404async fn get_configured_param(
407 ctx: &Context,
408 param: &EnteredLoginParam,
409) -> Result<ConfiguredLoginParam> {
410 ensure!(!param.addr.is_empty(), "Missing email address.");
411
412 ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password.");
413
414 let smtp_password = if param.smtp.password.is_empty() {
416 param.imap.password.clone()
417 } else {
418 param.smtp.password.clone()
419 };
420
421 let mut addr = param.addr.clone();
422 if param.oauth2 {
423 progress!(ctx, 10);
426 if let Some(oauth2_addr) = get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
427 .await?
428 .and_then(|e| e.parse().ok())
429 {
430 info!(ctx, "Authorized address is {}", oauth2_addr);
431 addr = oauth2_addr;
432 ctx.sql
433 .set_raw_config("addr", Some(param.addr.as_str()))
434 .await?;
435 }
436 progress!(ctx, 20);
437 }
438 let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?;
441 let param_domain = parsed.domain;
442
443 progress!(ctx, 200);
444
445 let provider;
446 let param_autoconfig;
447 if param.imap.server.is_empty()
448 && param.imap.port == 0
449 && param.imap.security == Socket::Automatic
450 && param.imap.user.is_empty()
451 && param.smtp.server.is_empty()
452 && param.smtp.port == 0
453 && param.smtp.security == Socket::Automatic
454 && param.smtp.user.is_empty()
455 {
456 info!(
458 ctx,
459 "checking internal provider-info for offline autoconfig"
460 );
461
462 provider = provider::get_provider_info(¶m_domain);
463 if let Some(provider) = provider {
464 if provider.server.is_empty() {
465 info!(ctx, "Offline autoconfig found, but no servers defined.");
466 param_autoconfig = None;
467 } else {
468 info!(ctx, "Offline autoconfig found.");
469 let servers = provider
470 .server
471 .iter()
472 .map(|s| ServerParams {
473 protocol: s.protocol,
474 socket: s.socket,
475 hostname: s.hostname.to_string(),
476 port: s.port,
477 username: match s.username_pattern {
478 UsernamePattern::Email => param.addr.to_string(),
479 UsernamePattern::Emaillocalpart => {
480 if let Some(at) = param.addr.find('@') {
481 param.addr.split_at(at).0.to_string()
482 } else {
483 param.addr.to_string()
484 }
485 }
486 },
487 })
488 .collect();
489
490 param_autoconfig = Some(servers)
491 }
492 } else {
493 info!(ctx, "No offline autoconfig found.");
495 param_autoconfig = get_autoconfig(ctx, param, ¶m_domain).await;
496 }
497 } else {
498 provider = None;
499 param_autoconfig = None;
500 }
501
502 progress!(ctx, 500);
503
504 let mut servers = param_autoconfig.unwrap_or_default();
505 if !servers
506 .iter()
507 .any(|server| server.protocol == Protocol::Imap)
508 {
509 servers.push(ServerParams {
510 protocol: Protocol::Imap,
511 hostname: param.imap.server.clone(),
512 port: param.imap.port,
513 socket: param.imap.security,
514 username: param.imap.user.clone(),
515 })
516 }
517 if !servers
518 .iter()
519 .any(|server| server.protocol == Protocol::Smtp)
520 {
521 servers.push(ServerParams {
522 protocol: Protocol::Smtp,
523 hostname: param.smtp.server.clone(),
524 port: param.smtp.port,
525 socket: param.smtp.security,
526 username: param.smtp.user.clone(),
527 })
528 }
529
530 let servers = expand_param_vector(servers, ¶m.addr, ¶m_domain);
531
532 let configured_login_param = ConfiguredLoginParam {
533 addr,
534 imap: servers
535 .iter()
536 .filter_map(|params| {
537 let Ok(security) = params.socket.try_into() else {
538 return None;
539 };
540 if params.protocol == Protocol::Imap {
541 Some(ConfiguredServerLoginParam {
542 connection: ConnectionCandidate {
543 host: params.hostname.clone(),
544 port: params.port,
545 security,
546 },
547 user: params.username.clone(),
548 })
549 } else {
550 None
551 }
552 })
553 .collect(),
554 imap_user: param.imap.user.clone(),
555 imap_password: param.imap.password.clone(),
556 imap_folder: Some(param.imap.folder.clone()).filter(|folder| !folder.is_empty()),
557 smtp: servers
558 .iter()
559 .filter_map(|params| {
560 let Ok(security) = params.socket.try_into() else {
561 return None;
562 };
563 if params.protocol == Protocol::Smtp {
564 Some(ConfiguredServerLoginParam {
565 connection: ConnectionCandidate {
566 host: params.hostname.clone(),
567 port: params.port,
568 security,
569 },
570 user: params.username.clone(),
571 })
572 } else {
573 None
574 }
575 })
576 .collect(),
577 smtp_user: param.smtp.user.clone(),
578 smtp_password,
579 provider,
580 certificate_checks: match param.certificate_checks {
581 EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
582 EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
583 EnteredCertificateChecks::AcceptInvalidCertificates
584 | EnteredCertificateChecks::AcceptInvalidCertificates2 => {
585 ConfiguredCertificateChecks::AcceptInvalidCertificates
586 }
587 },
588 oauth2: param.oauth2,
589 };
590 Ok(configured_login_param)
591}
592
593async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
594 progress!(ctx, 1);
595
596 let configured_param = get_configured_param(ctx, param).await?;
597 let proxy_config = ProxyConfig::load(ctx).await?;
598 let strict_tls = configured_param.strict_tls(proxy_config.is_some());
599
600 progress!(ctx, 550);
601
602 let context_smtp = ctx.clone();
605 let smtp_param = configured_param.smtp.clone();
606 let smtp_password = configured_param.smtp_password.clone();
607 let smtp_addr = configured_param.addr.clone();
608
609 let proxy_config2 = proxy_config.clone();
610 let smtp_config_task = task::spawn(async move {
611 let mut smtp = Smtp::new();
612 smtp.connect(
613 &context_smtp,
614 &smtp_param,
615 &smtp_password,
616 &proxy_config2,
617 &smtp_addr,
618 strict_tls,
619 configured_param.oauth2,
620 )
621 .await?;
622
623 Ok::<(), anyhow::Error>(())
624 });
625
626 progress!(ctx, 600);
627
628 let transport_id = 0;
631 let (_s, r) = async_channel::bounded(1);
632 let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
633 let configuring = true;
634 let imap_session = match imap.connect(ctx, configuring).await {
635 Ok(imap_session) => imap_session,
636 Err(err) => {
637 bail!("{}", nicer_configuration_error(ctx, format!("{err:#}")));
638 }
639 };
640
641 progress!(ctx, 850);
642
643 smtp_config_task.await??;
645
646 progress!(ctx, 900);
647
648 let is_configured = ctx.is_configured().await?;
649 if !ctx.get_config_bool(Config::FixIsChatmail).await? {
650 if imap_session.is_chatmail() {
651 ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
652 } else if !is_configured {
653 ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
656 }
657 }
658
659 drop(imap_session);
660 drop(imap);
661
662 progress!(ctx, 910);
663
664 let provider = configured_param.provider;
665 configured_param
666 .clone()
667 .save_to_transports_table(ctx, param, time())
668 .await?;
669 send_sync_transports(ctx).await?;
670
671 ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
672 .await?;
673
674 progress!(ctx, 920);
675
676 ctx.scheduler.interrupt_inbox().await;
677
678 progress!(ctx, 940);
679 ctx.update_device_chats()
680 .await
681 .context("Failed to update device chats")?;
682
683 ctx.sql.set_raw_config_bool("configured", true).await?;
684 ctx.emit_event(EventType::AccountsItemChanged);
685
686 Ok(provider)
687}
688
689async fn get_autoconfig(
694 ctx: &Context,
695 param: &EnteredLoginParam,
696 param_domain: &str,
697) -> Option<Vec<ServerParams>> {
698 let accept_invalid_certificates = param.certificate_checks.accept_invalid_certificates();
699
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 accept_invalid_certificates,
717 )
718 .await
719 {
720 return Some(res);
721 }
722 progress!(ctx, 300);
723
724 if let Ok(res) = moz_autoconfigure(
728 ctx,
729 &format!("https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml"),
730 ¶m.addr,
731 accept_invalid_certificates,
732 )
733 .await
734 {
735 return Some(res);
736 }
737 progress!(ctx, 310);
738
739 if let Ok(res) = outlk_autodiscover(
741 ctx,
742 format!("https://{param_domain}/autodiscover/autodiscover.xml"),
743 accept_invalid_certificates,
744 )
745 .await
746 {
747 return Some(res);
748 }
749 progress!(ctx, 320);
750
751 if let Ok(res) = outlk_autodiscover(
752 ctx,
753 format!("https://autodiscover.{param_domain}/autodiscover/autodiscover.xml",),
754 accept_invalid_certificates,
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/{param_domain}"),
766 ¶m.addr,
767 accept_invalid_certificates,
768 )
769 .await
770 {
771 return Some(res);
772 }
773
774 None
775}
776
777fn nicer_configuration_error(context: &Context, e: String) -> String {
778 if e.to_lowercase().contains("could not resolve")
779 || e.to_lowercase().contains("connection attempts")
780 || e.to_lowercase()
781 .contains("temporary failure in name resolution")
782 || e.to_lowercase().contains("name or service not known")
783 || e.to_lowercase()
784 .contains("failed to lookup address information")
785 {
786 return stock_str::error_no_network(context);
787 }
788
789 e
790}
791
792#[derive(Debug, thiserror::Error)]
793pub enum Error {
794 #[error("Invalid email address: {0:?}")]
795 InvalidEmailAddress(String),
796
797 #[error("XML error at position {position}: {error}")]
798 InvalidXml {
799 position: u64,
800 #[source]
801 error: quick_xml::Error,
802 },
803
804 #[error("Number of redirection is exceeded")]
805 Redirection,
806
807 #[error("{0:#}")]
808 Other(#[from] anyhow::Error),
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use crate::config::Config;
815 use crate::login_param::EnteredImapLoginParam;
816 use crate::test_utils::TestContext;
817
818 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
819 async fn test_no_panic_on_bad_credentials() {
820 let t = TestContext::new().await;
821 t.set_config(Config::Addr, Some("probably@unexistant.addr"))
822 .await
823 .unwrap();
824 t.set_config(Config::MailPw, Some("123456")).await.unwrap();
825 assert!(t.configure().await.is_err());
826 }
827
828 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
829 async fn test_get_configured_param() -> Result<()> {
830 let t = &TestContext::new().await;
831 let entered_param = EnteredLoginParam {
832 addr: "alice@example.org".to_string(),
833
834 imap: EnteredImapLoginParam {
835 user: "alice@example.net".to_string(),
836 password: "foobar".to_string(),
837 ..Default::default()
838 },
839
840 ..Default::default()
841 };
842 let configured_param = get_configured_param(t, &entered_param).await?;
843 assert_eq!(configured_param.imap_user, "alice@example.net");
844 assert_eq!(configured_param.smtp_user, "");
845 Ok(())
846 }
847}