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::{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 pub async fn is_configured(&self) -> Result<bool> {
68 self.sql.exists("SELECT COUNT(*) FROM transports", ()).await
69 }
70
71 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 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(¶m.addr);
133 let old_addr = self.get_config(Config::ConfiguredAddr).await?;
134 if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.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 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 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 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 #[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
279async 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 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 progress!(ctx, 10);
301 if let Some(oauth2_addr) = get_oauth2_addr(ctx, ¶m.addr, ¶m.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 let parsed = EmailAddress::new(¶m.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 info!(
333 ctx,
334 "checking internal provider-info for offline autoconfig"
335 );
336
337 provider = provider::get_provider_info(¶m_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 info!(ctx, "No offline autoconfig found.");
370 param_autoconfig = get_autoconfig(ctx, param, ¶m_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, ¶m.addr, ¶m_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 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 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 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
591async fn get_autoconfig(
596 ctx: &Context,
597 param: &EnteredLoginParam,
598 param_domain: &str,
599) -> Option<Vec<ServerParams>> {
600 let param_addr_urlencoded =
608 utf8_percent_encode(¶m.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 ¶m.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 &format!(
627 "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
628 ¶m_domain, ¶m_addr_urlencoded
629 ),
630 ¶m.addr,
631 )
632 .await
633 {
634 return Some(res);
635 }
636 progress!(ctx, 310);
637
638 if let Ok(res) = outlk_autodiscover(
640 ctx,
641 format!("https://{}/autodiscover/autodiscover.xml", ¶m_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 ¶m_domain
654 ),
655 )
656 .await
657 {
658 return Some(res);
659 }
660 progress!(ctx, 330);
661
662 if let Ok(res) = moz_autoconfigure(
664 ctx,
665 &format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
666 ¶m.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}