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