| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | mod auto_mozilla; |
| | mod auto_outlook; |
| | pub(crate) mod server_params; |
| |
|
| | use anyhow::{Context as _, Result, bail, ensure, format_err}; |
| | use auto_mozilla::moz_autoconfigure; |
| | use auto_outlook::outlk_autodiscover; |
| | use deltachat_contact_tools::{EmailAddress, addr_normalize}; |
| | use futures::FutureExt; |
| | use futures_lite::FutureExt as _; |
| | use percent_encoding::utf8_percent_encode; |
| | use server_params::{ServerParams, expand_param_vector}; |
| | use tokio::task; |
| |
|
| | use crate::config::{self, Config}; |
| | use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT; |
| | use crate::context::Context; |
| | use crate::imap::Imap; |
| | use crate::log::warn; |
| | use crate::login_param::EnteredCertificateChecks; |
| | pub use crate::login_param::EnteredLoginParam; |
| | use crate::message::Message; |
| | use crate::net::proxy::ProxyConfig; |
| | use crate::oauth2::get_oauth2_addr; |
| | use crate::provider::{Protocol, Provider, Socket, UsernamePattern}; |
| | use crate::qr::{login_param_from_account_qr, login_param_from_login_qr}; |
| | use crate::smtp::Smtp; |
| | use crate::sync::Sync::*; |
| | use crate::tools::time; |
| | use crate::transport::{ |
| | ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam, |
| | ConnectionCandidate, send_sync_transports, |
| | }; |
| | use crate::{EventType, stock_str}; |
| | use crate::{chat, provider}; |
| |
|
| | |
| | |
| | pub(crate) const MAX_TRANSPORT_RELAYS: usize = 5; |
| |
|
| | macro_rules! progress { |
| | ($context:tt, $progress:expr, $comment:expr) => { |
| | assert!( |
| | $progress <= 1000, |
| | "value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success" |
| | ); |
| | $context.emit_event($crate::events::EventType::ConfigureProgress { |
| | progress: $progress, |
| | comment: $comment, |
| | }); |
| | }; |
| | ($context:tt, $progress:expr) => { |
| | progress!($context, $progress, None); |
| | }; |
| | } |
| |
|
| | impl Context { |
| | |
| | pub async fn is_configured(&self) -> Result<bool> { |
| | self.sql.exists("SELECT COUNT(*) FROM transports", ()).await |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | pub async fn configure(&self) -> Result<()> { |
| | let mut param = EnteredLoginParam::load(self).await?; |
| |
|
| | self.add_transport_inner(&mut param).await |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> { |
| | self.stop_io().await; |
| | let result = self.add_transport_inner(param).await; |
| | if result.is_err() { |
| | if let Ok(true) = self.is_configured().await { |
| | self.start_io().await; |
| | } |
| | return result; |
| | } |
| | self.start_io().await; |
| | Ok(()) |
| | } |
| |
|
| | pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> { |
| | ensure!( |
| | !self.scheduler.is_running().await, |
| | "cannot configure, already running" |
| | ); |
| | ensure!( |
| | self.sql.is_open().await, |
| | "cannot configure, database not opened." |
| | ); |
| | param.addr = addr_normalize(¶m.addr); |
| | let cancel_channel = self.alloc_ongoing().await?; |
| |
|
| | let res = self |
| | .inner_configure(param) |
| | .race(cancel_channel.recv().map(|_| Err(format_err!("Canceled")))) |
| | .await; |
| |
|
| | self.free_ongoing().await; |
| |
|
| | if let Err(err) = res.as_ref() { |
| | |
| | |
| | let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await; |
| | progress!(self, 0, Some(error_msg.clone())); |
| | bail!(error_msg); |
| | } else { |
| | param.save(self).await?; |
| | progress!(self, 1000); |
| | } |
| |
|
| | res |
| | } |
| |
|
| | |
| | |
| | |
| | pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> { |
| | self.stop_io().await; |
| |
|
| | let result = async move { |
| | let mut param = match crate::qr::check_qr(self, qr).await? { |
| | crate::qr::Qr::Account { .. } => login_param_from_account_qr(self, qr).await?, |
| | crate::qr::Qr::Login { address, options } => { |
| | login_param_from_login_qr(&address, options)? |
| | } |
| | _ => bail!("QR code does not contain account"), |
| | }; |
| | self.add_transport_inner(&mut param).await?; |
| | Ok(()) |
| | } |
| | .await; |
| |
|
| | if result.is_err() { |
| | if let Ok(true) = self.is_configured().await { |
| | self.start_io().await; |
| | } |
| | return result; |
| | } |
| | self.start_io().await; |
| | Ok(()) |
| | } |
| |
|
| | |
| | |
| | |
| | pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> { |
| | let transports = self |
| | .sql |
| | .query_map_vec("SELECT entered_param FROM transports", (), |row| { |
| | let entered_param: String = row.get(0)?; |
| | let transport: EnteredLoginParam = serde_json::from_str(&entered_param)?; |
| | Ok(transport) |
| | }) |
| | .await?; |
| |
|
| | Ok(transports) |
| | } |
| |
|
| | |
| | pub async fn count_transports(&self) -> Result<usize> { |
| | self.sql.count("SELECT COUNT(*) FROM transports", ()).await |
| | } |
| |
|
| | |
| | |
| | pub async fn delete_transport(&self, addr: &str) -> Result<()> { |
| | let now = time(); |
| | let removed_transport_id = self |
| | .sql |
| | .transaction(|transaction| { |
| | let primary_addr = transaction.query_row( |
| | "SELECT value FROM config WHERE keyname='configured_addr'", |
| | (), |
| | |row| { |
| | let addr: String = row.get(0)?; |
| | Ok(addr) |
| | }, |
| | )?; |
| |
|
| | if primary_addr == addr { |
| | bail!("Cannot delete primary transport"); |
| | } |
| | let (transport_id, add_timestamp) = transaction.query_row( |
| | "DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp", |
| | (addr,), |
| | |row| { |
| | let id: u32 = row.get(0)?; |
| | let add_timestamp: i64 = row.get(1)?; |
| | Ok((id, add_timestamp)) |
| | }, |
| | )?; |
| | transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?; |
| | transaction.execute( |
| | "DELETE FROM imap_sync WHERE transport_id=?", |
| | (transport_id,), |
| | )?; |
| |
|
| | |
| | |
| | let remove_timestamp = std::cmp::max(now, add_timestamp); |
| |
|
| | transaction.execute( |
| | "INSERT INTO removed_transports (addr, remove_timestamp) |
| | VALUES (?, ?) |
| | ON CONFLICT (addr) |
| | DO UPDATE SET remove_timestamp = excluded.remove_timestamp", |
| | (addr, remove_timestamp), |
| | )?; |
| |
|
| | Ok(transport_id) |
| | }) |
| | .await?; |
| | send_sync_transports(self).await?; |
| | self.quota.write().await.remove(&removed_transport_id); |
| |
|
| | Ok(()) |
| | } |
| |
|
| | async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> { |
| | info!(self, "Configure ..."); |
| |
|
| | let old_addr = self.get_config(Config::ConfiguredAddr).await?; |
| | if old_addr.is_some() |
| | && !self |
| | .sql |
| | .exists( |
| | "SELECT COUNT(*) FROM transports WHERE addr=?", |
| | (¶m.addr,), |
| | ) |
| | .await? |
| | { |
| | |
| | |
| | if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") { |
| | bail!( |
| | "To use additional relays, disable the legacy option \"Settings / Advanced / Only Fetch from DeltaChat Folder\"." |
| | ); |
| | } |
| | if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") { |
| | bail!( |
| | "To use additional relays, disable the legacy option \"Settings / Advanced / Move automatically to DeltaChat Folder\"." |
| | ); |
| | } |
| | if self.get_config(Config::ShowEmails).await?.as_deref() != Some("2") { |
| | bail!( |
| | "To use additional relays, set the legacy option \"Settings / Advanced / Show Classic Emails\" to \"All\"." |
| | ); |
| | } |
| |
|
| | if self |
| | .sql |
| | .count("SELECT COUNT(*) FROM transports", ()) |
| | .await? |
| | >= MAX_TRANSPORT_RELAYS |
| | { |
| | bail!( |
| | "You have reached the maximum number of relays ({}).", |
| | MAX_TRANSPORT_RELAYS |
| | ) |
| | } |
| | } |
| |
|
| | let provider = match configure(self, param).await { |
| | Err(error) => { |
| | |
| | let configured_param = get_configured_param(self, param).await; |
| | warn!( |
| | self, |
| | "configure failed: Entered params: {}. Used params: {}. Error: {error}.", |
| | param.to_string(), |
| | configured_param |
| | .map(|param| param.to_string()) |
| | .unwrap_or("error".to_owned()) |
| | ); |
| | return Err(error); |
| | } |
| | Ok(provider) => provider, |
| | }; |
| | self.set_config_internal(Config::NotifyAboutWrongPw, Some("1")) |
| | .await?; |
| | on_configure_completed(self, provider).await?; |
| | Ok(()) |
| | } |
| | } |
| |
|
| | async fn on_configure_completed( |
| | context: &Context, |
| | provider: Option<&'static Provider>, |
| | ) -> Result<()> { |
| | if let Some(provider) = provider { |
| | if let Some(config_defaults) = provider.config_defaults { |
| | for def in config_defaults { |
| | if !context.config_exists(def.key).await? { |
| | info!(context, "apply config_defaults {}={}", def.key, def.value); |
| | context |
| | .set_config_ex(Nosync, def.key, Some(def.value)) |
| | .await?; |
| | } else { |
| | info!( |
| | context, |
| | "skip already set config_defaults {}={}", def.key, def.value |
| | ); |
| | } |
| | } |
| | } |
| |
|
| | if !provider.after_login_hint.is_empty() { |
| | let mut msg = Message::new_text(provider.after_login_hint.to_string()); |
| | if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg)) |
| | .await |
| | .is_err() |
| | { |
| | warn!(context, "cannot add after_login_hint as core-provider-info"); |
| | } |
| | } |
| | } |
| |
|
| | Ok(()) |
| | } |
| |
|
| | |
| | |
| | async fn get_configured_param( |
| | ctx: &Context, |
| | param: &EnteredLoginParam, |
| | ) -> Result<ConfiguredLoginParam> { |
| | ensure!(!param.addr.is_empty(), "Missing email address."); |
| |
|
| | ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password."); |
| |
|
| | |
| | let smtp_password = if param.smtp.password.is_empty() { |
| | param.imap.password.clone() |
| | } else { |
| | param.smtp.password.clone() |
| | }; |
| |
|
| | let mut addr = param.addr.clone(); |
| | if param.oauth2 { |
| | |
| | |
| | progress!(ctx, 10); |
| | if let Some(oauth2_addr) = get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password) |
| | .await? |
| | .and_then(|e| e.parse().ok()) |
| | { |
| | info!(ctx, "Authorized address is {}", oauth2_addr); |
| | addr = oauth2_addr; |
| | ctx.sql |
| | .set_raw_config("addr", Some(param.addr.as_str())) |
| | .await?; |
| | } |
| | progress!(ctx, 20); |
| | } |
| | |
| |
|
| | let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?; |
| | let param_domain = parsed.domain; |
| |
|
| | progress!(ctx, 200); |
| |
|
| | let provider; |
| | let param_autoconfig; |
| | if param.imap.server.is_empty() |
| | && param.imap.port == 0 |
| | && param.imap.security == Socket::Automatic |
| | && param.imap.user.is_empty() |
| | && param.smtp.server.is_empty() |
| | && param.smtp.port == 0 |
| | && param.smtp.security == Socket::Automatic |
| | && param.smtp.user.is_empty() |
| | { |
| | |
| | info!( |
| | ctx, |
| | "checking internal provider-info for offline autoconfig" |
| | ); |
| |
|
| | provider = provider::get_provider_info(¶m_domain); |
| | if let Some(provider) = provider { |
| | if provider.server.is_empty() { |
| | info!(ctx, "Offline autoconfig found, but no servers defined."); |
| | param_autoconfig = None; |
| | } else { |
| | info!(ctx, "Offline autoconfig found."); |
| | let servers = provider |
| | .server |
| | .iter() |
| | .map(|s| ServerParams { |
| | protocol: s.protocol, |
| | socket: s.socket, |
| | hostname: s.hostname.to_string(), |
| | port: s.port, |
| | username: match s.username_pattern { |
| | UsernamePattern::Email => param.addr.to_string(), |
| | UsernamePattern::Emaillocalpart => { |
| | if let Some(at) = param.addr.find('@') { |
| | param.addr.split_at(at).0.to_string() |
| | } else { |
| | param.addr.to_string() |
| | } |
| | } |
| | }, |
| | }) |
| | .collect(); |
| |
|
| | param_autoconfig = Some(servers) |
| | } |
| | } else { |
| | |
| | info!(ctx, "No offline autoconfig found."); |
| | param_autoconfig = get_autoconfig(ctx, param, ¶m_domain).await; |
| | } |
| | } else { |
| | provider = None; |
| | param_autoconfig = None; |
| | } |
| |
|
| | progress!(ctx, 500); |
| |
|
| | let mut servers = param_autoconfig.unwrap_or_default(); |
| | if !servers |
| | .iter() |
| | .any(|server| server.protocol == Protocol::Imap) |
| | { |
| | servers.push(ServerParams { |
| | protocol: Protocol::Imap, |
| | hostname: param.imap.server.clone(), |
| | port: param.imap.port, |
| | socket: param.imap.security, |
| | username: param.imap.user.clone(), |
| | }) |
| | } |
| | if !servers |
| | .iter() |
| | .any(|server| server.protocol == Protocol::Smtp) |
| | { |
| | servers.push(ServerParams { |
| | protocol: Protocol::Smtp, |
| | hostname: param.smtp.server.clone(), |
| | port: param.smtp.port, |
| | socket: param.smtp.security, |
| | username: param.smtp.user.clone(), |
| | }) |
| | } |
| |
|
| | let servers = expand_param_vector(servers, ¶m.addr, ¶m_domain); |
| |
|
| | let configured_login_param = ConfiguredLoginParam { |
| | addr, |
| | imap: servers |
| | .iter() |
| | .filter_map(|params| { |
| | let Ok(security) = params.socket.try_into() else { |
| | return None; |
| | }; |
| | if params.protocol == Protocol::Imap { |
| | Some(ConfiguredServerLoginParam { |
| | connection: ConnectionCandidate { |
| | host: params.hostname.clone(), |
| | port: params.port, |
| | security, |
| | }, |
| | user: params.username.clone(), |
| | }) |
| | } else { |
| | None |
| | } |
| | }) |
| | .collect(), |
| | imap_user: param.imap.user.clone(), |
| | imap_password: param.imap.password.clone(), |
| | smtp: servers |
| | .iter() |
| | .filter_map(|params| { |
| | let Ok(security) = params.socket.try_into() else { |
| | return None; |
| | }; |
| | if params.protocol == Protocol::Smtp { |
| | Some(ConfiguredServerLoginParam { |
| | connection: ConnectionCandidate { |
| | host: params.hostname.clone(), |
| | port: params.port, |
| | security, |
| | }, |
| | user: params.username.clone(), |
| | }) |
| | } else { |
| | None |
| | } |
| | }) |
| | .collect(), |
| | smtp_user: param.smtp.user.clone(), |
| | smtp_password, |
| | provider, |
| | certificate_checks: match param.certificate_checks { |
| | EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic, |
| | EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict, |
| | EnteredCertificateChecks::AcceptInvalidCertificates |
| | | EnteredCertificateChecks::AcceptInvalidCertificates2 => { |
| | ConfiguredCertificateChecks::AcceptInvalidCertificates |
| | } |
| | }, |
| | oauth2: param.oauth2, |
| | }; |
| | Ok(configured_login_param) |
| | } |
| |
|
| | async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> { |
| | progress!(ctx, 1); |
| |
|
| | let ctx2 = ctx.clone(); |
| | let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await }); |
| |
|
| | let configured_param = get_configured_param(ctx, param).await?; |
| | let proxy_config = ProxyConfig::load(ctx).await?; |
| | let strict_tls = configured_param.strict_tls(proxy_config.is_some()); |
| |
|
| | progress!(ctx, 550); |
| |
|
| | |
| | |
| | let context_smtp = ctx.clone(); |
| | let smtp_param = configured_param.smtp.clone(); |
| | let smtp_password = configured_param.smtp_password.clone(); |
| | let smtp_addr = configured_param.addr.clone(); |
| |
|
| | let proxy_config2 = proxy_config.clone(); |
| | let smtp_config_task = task::spawn(async move { |
| | let mut smtp = Smtp::new(); |
| | smtp.connect( |
| | &context_smtp, |
| | &smtp_param, |
| | &smtp_password, |
| | &proxy_config2, |
| | &smtp_addr, |
| | strict_tls, |
| | configured_param.oauth2, |
| | ) |
| | .await?; |
| |
|
| | Ok::<(), anyhow::Error>(()) |
| | }); |
| |
|
| | progress!(ctx, 600); |
| |
|
| | |
| |
|
| | let transport_id = 0; |
| | let (_s, r) = async_channel::bounded(1); |
| | let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?; |
| | let configuring = true; |
| | if let Err(err) = imap.connect(ctx, configuring).await { |
| | bail!( |
| | "{}", |
| | nicer_configuration_error(ctx, format!("{err:#}")).await |
| | ); |
| | }; |
| |
|
| | progress!(ctx, 850); |
| |
|
| | |
| | smtp_config_task.await??; |
| |
|
| | progress!(ctx, 900); |
| |
|
| | let is_configured = ctx.is_configured().await?; |
| | if !is_configured { |
| | ctx.sql.set_raw_config("mvbox_move", Some("0")).await?; |
| | ctx.sql.set_raw_config("only_fetch_mvbox", None).await?; |
| | } |
| |
|
| | drop(imap); |
| |
|
| | progress!(ctx, 910); |
| |
|
| | let provider = configured_param.provider; |
| | configured_param |
| | .clone() |
| | .save_to_transports_table(ctx, param, time()) |
| | .await?; |
| | send_sync_transports(ctx).await?; |
| |
|
| | ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string())) |
| | .await?; |
| |
|
| | progress!(ctx, 920); |
| |
|
| | ctx.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(false)) |
| | .await?; |
| | ctx.scheduler.interrupt_inbox().await; |
| |
|
| | progress!(ctx, 940); |
| | update_device_chats_handle.await??; |
| |
|
| | ctx.sql.set_raw_config_bool("configured", true).await?; |
| | ctx.emit_event(EventType::AccountsItemChanged); |
| |
|
| | Ok(provider) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | async fn get_autoconfig( |
| | ctx: &Context, |
| | param: &EnteredLoginParam, |
| | param_domain: &str, |
| | ) -> Option<Vec<ServerParams>> { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | let param_addr_urlencoded = |
| | utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string(); |
| |
|
| | if let Ok(res) = moz_autoconfigure( |
| | ctx, |
| | &format!( |
| | "https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}" |
| | ), |
| | ¶m.addr, |
| | ) |
| | .await |
| | { |
| | return Some(res); |
| | } |
| | progress!(ctx, 300); |
| |
|
| | if let Ok(res) = moz_autoconfigure( |
| | ctx, |
| | |
| | &format!( |
| | "https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}", |
| | ¶m_domain, ¶m_addr_urlencoded |
| | ), |
| | ¶m.addr, |
| | ) |
| | .await |
| | { |
| | return Some(res); |
| | } |
| | progress!(ctx, 310); |
| |
|
| | |
| | if let Ok(res) = outlk_autodiscover( |
| | ctx, |
| | format!("https://{}/autodiscover/autodiscover.xml", ¶m_domain), |
| | ) |
| | .await |
| | { |
| | return Some(res); |
| | } |
| | progress!(ctx, 320); |
| |
|
| | if let Ok(res) = outlk_autodiscover( |
| | ctx, |
| | format!( |
| | "https://autodiscover.{}/autodiscover/autodiscover.xml", |
| | ¶m_domain |
| | ), |
| | ) |
| | .await |
| | { |
| | return Some(res); |
| | } |
| | progress!(ctx, 330); |
| |
|
| | |
| | if let Ok(res) = moz_autoconfigure( |
| | ctx, |
| | &format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain), |
| | ¶m.addr, |
| | ) |
| | .await |
| | { |
| | return Some(res); |
| | } |
| |
|
| | None |
| | } |
| |
|
| | async fn nicer_configuration_error(context: &Context, e: String) -> String { |
| | if e.to_lowercase().contains("could not resolve") |
| | || e.to_lowercase().contains("connection attempts") |
| | || e.to_lowercase() |
| | .contains("temporary failure in name resolution") |
| | || e.to_lowercase().contains("name or service not known") |
| | || e.to_lowercase() |
| | .contains("failed to lookup address information") |
| | { |
| | return stock_str::error_no_network(context).await; |
| | } |
| |
|
| | e |
| | } |
| |
|
| | #[derive(Debug, thiserror::Error)] |
| | pub enum Error { |
| | #[error("Invalid email address: {0:?}")] |
| | InvalidEmailAddress(String), |
| |
|
| | #[error("XML error at position {position}: {error}")] |
| | InvalidXml { |
| | position: u64, |
| | #[source] |
| | error: quick_xml::Error, |
| | }, |
| |
|
| | #[error("Number of redirection is exceeded")] |
| | Redirection, |
| |
|
| | #[error("{0:#}")] |
| | Other(#[from] anyhow::Error), |
| | } |
| |
|
| | #[cfg(test)] |
| | mod tests { |
| | use super::*; |
| | use crate::config::Config; |
| | use crate::login_param::EnteredServerLoginParam; |
| | use crate::test_utils::TestContext; |
| |
|
| | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
| | async fn test_no_panic_on_bad_credentials() { |
| | let t = TestContext::new().await; |
| | t.set_config(Config::Addr, Some("probably@unexistant.addr")) |
| | .await |
| | .unwrap(); |
| | t.set_config(Config::MailPw, Some("123456")).await.unwrap(); |
| | assert!(t.configure().await.is_err()); |
| | } |
| |
|
| | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
| | async fn test_get_configured_param() -> Result<()> { |
| | let t = &TestContext::new().await; |
| | let entered_param = EnteredLoginParam { |
| | addr: "alice@example.org".to_string(), |
| |
|
| | imap: EnteredServerLoginParam { |
| | user: "alice@example.net".to_string(), |
| | password: "foobar".to_string(), |
| | ..Default::default() |
| | }, |
| |
|
| | ..Default::default() |
| | }; |
| | let configured_param = get_configured_param(t, &entered_param).await?; |
| | assert_eq!(configured_param.imap_user, "alice@example.net"); |
| | assert_eq!(configured_param.smtp_user, ""); |
| | Ok(()) |
| | } |
| | } |
| |
|