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