refactor(sdk): sdk is now async
continuous-integration/drone/push Build is failing Details

main
Guus van Meerveld 2 months ago
parent 1fb34fd800
commit a024d02db3
Signed by: Guusvanmeerveld
GPG Key ID: 2BA7D7912771966E

195
Cargo.lock generated

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "adler"
version = "1.0.2"
@ -87,6 +93,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "aliasable"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
@ -131,16 +143,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]]
name = "arrayvec"
version = "0.5.2"
name = "async-channel"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
dependencies = [
"concurrent-queue",
"event-listener",
"futures-core",
]
[[package]]
name = "async-imap"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31dd46675b8c5a2ecadd4ef690c84966eacf21b1e2327373a0d167494d1a1d28"
dependencies = [
"async-channel",
"async-native-tls",
"base64 0.13.1",
"byte-pool",
"chrono",
"futures",
"imap-proto",
"log",
"nom",
"once_cell",
"ouroboros",
"pin-utils",
"stop-token",
"thiserror",
"tokio",
]
[[package]]
name = "async-native-tls"
version = "0.5.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec"
checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe"
dependencies = [
"native-tls",
"thiserror",
@ -178,6 +218,14 @@ dependencies = [
"syn",
]
[[package]]
name = "async-tcp"
version = "0.1.0"
dependencies = [
"async-native-tls",
"tokio",
]
[[package]]
name = "async-trait"
version = "0.1.66"
@ -327,18 +375,22 @@ dependencies = [
"serde",
]
[[package]]
name = "bufstream"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
[[package]]
name = "bumpalo"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "byte-pool"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca"
dependencies = [
"crossbeam-queue",
"stable_deref_trait",
]
[[package]]
name = "bytemuck"
version = "1.13.0"
@ -563,6 +615,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "concurrent-queue"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@ -656,6 +717,16 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.14"
@ -1044,6 +1115,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "event-listener"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "fastrand"
version = "1.8.0"
@ -1807,38 +1884,13 @@ dependencies = [
"num-traits",
]
[[package]]
name = "imap"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c617c55def8c42129e0dd503f11d7ee39d73f5c7e01eff55768b3879ff1d107d"
dependencies = [
"base64 0.13.1",
"bufstream",
"chrono",
"imap-proto 0.10.2",
"lazy_static",
"native-tls",
"nom 5.1.2",
"regex",
]
[[package]]
name = "imap-proto"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f"
dependencies = [
"nom 5.1.2",
]
[[package]]
name = "imap-proto"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f73b1b63179418b20aa81002d616c5f21b4ba257da9bca6989ea64dc573933e0"
dependencies = [
"nom 7.1.3",
"nom",
]
[[package]]
@ -2000,26 +2052,13 @@ dependencies = [
"idna",
"mime",
"native-tls",
"nom 7.1.3",
"nom",
"once_cell",
"quoted_printable",
"socket2",
"tokio",
]
[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec",
"bitflags",
"cfg-if",
"ryu",
"static_assertions",
]
[[package]]
name = "libappindicator"
version = "0.7.1"
@ -2319,17 +2358,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
"lexical-core",
"memchr",
"version_check",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -2515,6 +2543,29 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "ouroboros"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db"
dependencies = [
"aliasable",
"ouroboros_macro",
]
[[package]]
name = "ouroboros_macro"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7"
dependencies = [
"Inflector",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -3266,18 +3317,18 @@ name = "sdk"
version = "0.1.0"
dependencies = [
"ammonia",
"async-imap",
"async-native-tls",
"async-pop3",
"async-tcp",
"async-trait",
"autoconfig",
"bufstream",
"chrono",
"dotenv",
"dust-mail-utils",
"futures",
"imap",
"imap-proto 0.16.2",
"lettre",
"mailparse",
"native-tls",
"serde",
"serde_json",
"tokio",
@ -3601,10 +3652,16 @@ dependencies = [
]
[[package]]
name = "static_assertions"
version = "1.1.0"
name = "stop-token"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b"
dependencies = [
"async-channel",
"cfg-if",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "string_cache"

@ -4,6 +4,7 @@ members = [
"apps/web/src-tauri",
"apps/server",
"packages/sdk",
"packages/async-tcp",
"packages/autoconfig",
"packages/structures-rs",
"packages/async-pop3"

@ -11,19 +11,22 @@ mod utils;
const AT_SYMBOL: char = '@';
/// Given an email providers domain, try to connect to autoconfig servers for that provider and return the config.
pub async fn from_domain(domain: &str) -> Result<Option<Config>> {
pub async fn from_domain<D: AsRef<str>>(domain: D) -> Result<Option<Config>> {
let client = Client::new()?;
let urls = vec![
// Try connect to connect with the users mail server directly
format!("http://autoconfig.{}/mail/config-v1.1.xml", domain),
format!("http://autoconfig.{}/mail/config-v1.1.xml", domain.as_ref()),
// The fallback url
format!(
"http://{}/.well-known/autoconfig/mail/config-v1.1.xml",
domain
domain.as_ref()
),
// If the previous two methods did not work then the email server provider has not setup Thunderbird autoconfig, so we ask Mozilla for their config.
format!("https://autoconfig.thunderbird.net/v1.1/{}", domain),
format!(
"https://autoconfig.thunderbird.net/v1.1/{}",
domain.as_ref()
),
];
let config_unparsed: Option<String> = client.request_urls(urls).await;

@ -7,12 +7,10 @@ edition = "2021"
[dependencies]
# Imap
imap = {version = "2.4.1", optional = true }
imap-proto = { version = "0.16", optional = true }
bufstream = {version = "0.1", optional = true}
async-imap = {version = "0.6.0", default-features = false, features = ["runtime-tokio"], optional = true }
# Pop
pop3 = {version = "0.1", optional = true, path = "../pop3" }
async-pop3 = {version = "0.1", optional = true, path = "../async-pop3" }
# Smtp
lettre = { version = "0.10", optional = true }
@ -31,10 +29,15 @@ serde_json = "1.0"
chrono = "0.4"
# Tls
native-tls = "0.2"
async-native-tls = {version = "0.4.0", default-features = false, features = ["runtime-tokio"] }
async-tcp = { version = "0.1.0", path = "../async-tcp" }
dust-mail-utils = { version = "0.1.0", path = "../structures-rs" }
# Async
tokio = { version = "1", features = ["full"] }
async-trait = "0.1.66"
futures = "0.3"
# Sanitizing text
@ -51,5 +54,5 @@ autoconfig = ["dep:autoconfig"]
smtp = ["dep:lettre"]
pop = ["dep:pop3"]
imap = ["dep:imap", "dep:imap-proto", "dep:bufstream"]
pop = ["dep:async-pop3"]
imap = ["dep:async-imap"]

@ -0,0 +1,50 @@
use async_trait::async_trait;
use tokio::time::{Duration, Instant};
use crate::types::Result;
#[async_trait]
pub trait Refresher<T> {
async fn refresh(&mut self) -> Result<T>;
}
/// A Cache struct that will automatically refresh the cached value when it has expired using a given refresher struct.
pub struct Cache<T> {
cached: Option<T>,
expiry_time: Duration,
refreshed: Instant,
}
impl<T> Cache<T> {
pub fn new(expiry_time: Duration) -> Self {
Self {
cached: None,
expiry_time,
refreshed: Instant::now(),
}
}
/// Whether the cache has expired.
pub fn is_expired(&self) -> bool {
self.cached.is_none() || self.refreshed.checked_sub(self.expiry_time).is_none()
}
/// Get the cached item and refresh it if it has expired.
pub async fn get<R: Refresher<T>>(&mut self, refresher: &mut R) -> Result<&T> {
// If there is no cached value yet or the cache has expired, refresh it
if self.is_expired() {
let refreshed = refresher.refresh().await?;
self.cached = Some(refreshed);
self.refreshed = Instant::now();
}
match self.cached.as_ref() {
Some(cached) => Ok(cached),
// We check in the is_expired function whether self.cached is none, so it can never be none.
None => unreachable!(),
}
}
}

@ -1,9 +1,11 @@
use std::{
io::{Read, Write},
use async_native_tls::TlsStream;
use async_trait::async_trait;
use tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpStream,
};
use native_tls::TlsStream;
use std::fmt::Debug;
#[cfg(feature = "imap")]
use crate::imap::{self, ImapClient};
@ -17,7 +19,7 @@ use crate::types::{
enum IncomingClientTypeWithClient<S>
where
S: Read + Write,
S: AsyncRead + AsyncWrite + Unpin + Debug + Send,
{
#[cfg(feature = "imap")]
Imap(ImapClient<S>),
@ -25,27 +27,27 @@ where
Pop(PopClient<S>),
}
pub struct IncomingClient<S: Read + Write + Send> {
pub struct IncomingClient<S: AsyncRead + AsyncWrite + Unpin + Debug + Send> {
client: IncomingClientTypeWithClient<S>,
}
impl<S: Read + Write + 'static + Send> IncomingClient<S> {
impl<S: AsyncRead + AsyncWrite + Unpin + Debug + Send + Sync + 'static> IncomingClient<S> {
/// Login to the specified mail server using a username and a password.
pub fn login<T: AsRef<str>>(
pub async fn login<T: AsRef<str>>(
self,
username: T,
password: T,
) -> Result<Box<dyn IncomingSession + Send>> {
) -> Result<Box<dyn IncomingSession>> {
match self.client {
#[cfg(feature = "imap")]
IncomingClientTypeWithClient::Imap(client) => {
let session = client.login(username, password)?;
let session = client.login(username, password).await?;
Ok(Box::new(session))
}
#[cfg(feature = "pop")]
IncomingClientTypeWithClient::Pop(client) => {
let session = client.login(username, password)?;
let session = client.login(username, password).await?;
Ok(Box::new(session))
}
@ -57,14 +59,14 @@ impl<S: Read + Write + 'static + Send> IncomingClient<S> {
}
/// Login to the specified mail servers using OAuth2 credentials, which consist of an access token and a usernames
pub fn oauth2_login(
pub async fn oauth2_login(
self,
oauth_credentials: &OAuthCredentials,
) -> Result<Box<dyn IncomingSession + Send>> {
oauth_credentials: OAuthCredentials,
) -> Result<Box<dyn IncomingSession>> {
match self.client {
#[cfg(feature = "imap")]
IncomingClientTypeWithClient::Imap(client) => {
let session = client.oauth2_login(oauth_credentials)?;
let session = client.oauth2_login(oauth_credentials).await?;
Ok(Box::new(session))
}
@ -82,30 +84,31 @@ impl<S: Read + Write + 'static + Send> IncomingClient<S> {
}
}
#[async_trait]
pub trait IncomingSession {
/// Logout of the session, closing the connection with the server if applicable.
fn logout(&mut self) -> Result<()>;
async fn logout(&mut self) -> Result<()>;
/// Returns a list of all of the mailboxes that are on the server.
fn box_list(&mut self) -> Result<&Vec<MailBox>>;
async fn box_list(&mut self) -> Result<&Vec<MailBox>>;
/// Returns some basic information about a specified mailbox.
fn get(&mut self, box_id: &str) -> Result<&MailBox>;
async fn get(&mut self, box_id: &str) -> Result<&MailBox>;
/// Deletes a specified mailbox.
fn delete(&mut self, box_id: &str) -> Result<()>;
async fn delete(&mut self, box_id: &str) -> Result<()>;
/// Creates a new mailbox with a specified id.
fn create(&mut self, box_id: &str) -> Result<()>;
async fn create(&mut self, box_id: &str) -> Result<()>;
/// Renames a specified mailbox.
fn rename(&mut self, box_id: &str, new_name: &str) -> Result<()>;
async fn rename(&mut self, box_id: &str, new_name: &str) -> Result<()>;
/// Returns a list of a specified range of messages from a specified mailbox.
fn messages(&mut self, box_id: &str, start: u32, end: u32) -> Result<Vec<Preview>>;
async fn messages(&mut self, box_id: &str, start: u32, end: u32) -> Result<Vec<Preview>>;
/// Returns all of the relevant data for a specified message.
fn get_message(&mut self, box_id: &str, msg_id: &str) -> Result<Message>;
async fn get_message(&mut self, box_id: &str, msg_id: &str) -> Result<Message>;
}
/// A struct used to create a connection to an incoming mail server.
@ -175,13 +178,13 @@ impl IncomingClientBuilder {
}
/// Creates a new client over a secure tcp connection.
pub fn build(&self) -> Result<IncomingClient<TlsStream<TcpStream>>> {
pub async fn build(&self) -> Result<IncomingClient<TlsStream<TcpStream>>> {
let client = match self.client_type {
#[cfg(feature = "imap")]
IncomingClientType::Imap => {
let (server, port) = self.get_connect_config()?;
let client = imap::connect(server, port.clone())?;
let client = imap::connect(server, port.clone()).await?;
IncomingClientTypeWithClient::Imap(client)
}
@ -189,7 +192,7 @@ impl IncomingClientBuilder {
IncomingClientType::Pop => {
let (server, port) = self.get_connect_config()?;
let client = pop::connect(server, port.clone())?;
let client = pop::connect(server, port.clone()).await?;
IncomingClientTypeWithClient::Pop(client)
}
@ -201,13 +204,13 @@ impl IncomingClientBuilder {
/// Creates a new client over a plain tcp connection.
///
/// ### Do not use this in a production environment as it will send your credentials to the server without any encryption!
pub fn build_plain(&self) -> Result<IncomingClient<TcpStream>> {
pub async fn build_plain(&self) -> Result<IncomingClient<TcpStream>> {
let client = match self.client_type {
#[cfg(feature = "imap")]
IncomingClientType::Imap => {
let (server, port) = self.get_connect_config()?;
let client = imap::connect_plain(server, port.clone())?;
let client = imap::connect_plain(server, port.clone()).await?;
IncomingClientTypeWithClient::Imap(client)
}
@ -215,7 +218,7 @@ impl IncomingClientBuilder {
IncomingClientType::Pop => {
let (server, port) = self.get_connect_config()?;
let client = pop::connect_plain(server, port.clone())?;
let client = pop::connect_plain(server, port.clone()).await?;
IncomingClientTypeWithClient::Pop(client)
}

@ -6,7 +6,7 @@ use crate::types::IncomingClientType;
use super::incoming::{IncomingClientBuilder, IncomingSession};
fn create_session() -> Box<dyn IncomingSession> {
async fn create_session() -> Box<dyn IncomingSession> {
dotenv().ok();
let username = env::var("IMAP_USERNAME").unwrap();
@ -19,25 +19,26 @@ fn create_session() -> Box<dyn IncomingSession> {
.set_server(server)
.set_port(port)
.build()
.await
.unwrap();
let session = client.login(&username, &password).unwrap();
let session = client.login(&username, &password).await.unwrap();
session
}
#[test]
fn logout() {
let mut session = create_session();
#[tokio::test]
async fn logout() {
let mut session = create_session().await;
session.logout().unwrap();
session.logout().await.unwrap();
}
#[test]
fn box_list() {
let mut session = create_session();
#[tokio::test]
async fn box_list() {
let mut session = create_session().await;
let list = session.box_list().unwrap();
let list = session.box_list().await.unwrap();
for mailbox in list {
println!("{}", mailbox.counts().unwrap().total());

@ -1,7 +1,7 @@
use std::sync::Arc;
use std::collections::HashMap;
use dust_mail_utils::validate_email;
use futures::future::join_all;
use tokio::task::{spawn, spawn_blocking};
use crate::types::{ConnectionSecurity, Error, ErrorKind, Result};
@ -12,47 +12,39 @@ mod types;
#[cfg(feature = "autoconfig")]
use parse::AutoConfigParser;
use types::{AuthenticationType, ConfigType, ServerConfig, ServerConfigType, Socket};
use types::{AuthenticationType, ConfigType, ServerConfig, ServerConfigType};
pub use types::Config;
use self::{service::detect_server_config, types::Socket};
const AT_SYMBOL: &str = "@";
/// Given an array of sockets (domain name, port and connection security) check which of the sockets have a server of a given mail server type running on them.
async fn check_sockets<'a>(
sockets: Vec<Socket>,
client_type: &'a ServerConfigType,
) -> Vec<(Socket, &'a ServerConfigType)> {
let working_socket_results: Vec<_> = sockets
.clone()
.into_iter()
.map(move |(domain, port, security)| {
spawn_blocking(move || service::from_server(&domain, &port, &security))
})
.collect();
let working_sockets = join_all(working_socket_results)
.await
.into_iter()
.enumerate()
.filter_map(|(i, result)| match result.unwrap() {
Ok(result) => {
if &result? == client_type {
Some((sockets.get(i).cloned().unwrap(), client_type))
} else {
None
}
}
Err(_) => None,
})
.collect();
async fn check_sockets(sockets: Vec<Socket>) -> Result<HashMap<ServerConfigType, Socket>> {
let mut config_types = HashMap::new();
// First, we create an array of tasks that we then later run all at the same time.
let mut tasks = Vec::new();
for socket in sockets.iter() {
tasks.push(detect_server_config(socket))
}
let results = join_all(tasks).await;
for (result, socket) in results.into_iter().zip(sockets.into_iter()) {
if let Some(config_type) = result? {
config_types.entry(config_type).or_insert(socket);
}
}
working_sockets
Ok(config_types)
}
/// Automatically detect an email providers config for a given email address
pub async fn from_email(email_address: &str) -> Result<Config> {
if !autoconfig::validate_email(email_address) {
if !validate_email(email_address) {
return Err(Error::new(
ErrorKind::ParseAddress,
"Given email address is invalid",
@ -66,25 +58,11 @@ pub async fn from_email(email_address: &str) -> Result<Config> {
// Skip the prefix
split.next();
let domain = Arc::new(split.next().unwrap().to_string());
let domain = split.next().unwrap().to_string();
#[cfg(feature = "autoconfig")]
{
let domain_clone = domain.clone();
let detected_autoconfig = spawn_blocking(move || {
autoconfig::from_domain(&domain_clone).map_err(|e| {
Error::new(
ErrorKind::FetchConfigFailed,
format!(
"Error when requesting config from email provider: {}",
e.message()
),
)
})
})
.await
.unwrap()?;
let detected_autoconfig = autoconfig::from_domain(&domain).await?;
match detected_autoconfig {
Some(detected_autoconfig) => {
@ -93,14 +71,15 @@ pub async fn from_email(email_address: &str) -> Result<Config> {
None => {}
}
}
// TODO: Check for domains such mail.domain.com, imap.domain.com etc.
// If we didn't find anything using autoconfig, we try the subdomains.
if config.is_none() {
let mail_domain = format!("mail.{}", domain);
let mut incoming_configs: Vec<ServerConfig> = Vec::new();
let mut outgoing_configs: Vec<ServerConfig> = Vec::new();
let mut check_socket_threads = Vec::new();
let mut sockets_to_check: Vec<Socket> = Vec::new();
#[cfg(feature = "imap")]
{
@ -109,30 +88,18 @@ pub async fn from_email(email_address: &str) -> Result<Config> {
let imap_domain = format!("imap.{}", domain);
let sockets_to_check: Vec<Socket> = vec![
(
mail_domain.to_string(),
secure_imap_port,
ConnectionSecurity::Tls,
),
(
imap_domain.clone(),
secure_imap_port,
ConnectionSecurity::Tls,
),
(
mail_domain.to_string(),
imap_port,
ConnectionSecurity::Plain,
),
(imap_domain, imap_port, ConnectionSecurity::Plain),
let tuples = vec![
(&mail_domain, secure_imap_port, ConnectionSecurity::Tls),
(&imap_domain, secure_imap_port, ConnectionSecurity::Tls),
(&mail_domain, imap_port, ConnectionSecurity::Plain),
(&imap_domain, imap_port, ConnectionSecurity::Plain),
];
// Check mail.domain.tld and imap.domain.tld on the default secure imap port
let check_imap_sockets =
spawn(check_sockets(sockets_to_check, &ServerConfigType::Imap));
for tuple in tuples {
let socket = Socket::from_tuple(tuple);
check_socket_threads.push(check_imap_sockets);
sockets_to_check.push(socket);
}
}
#[cfg(feature = "pop")]
@ -142,20 +109,18 @@ pub async fn from_email(email_address: &str) -> Result<Config> {
let pop_domain = format!("pop.{}", domain);
let sockets_to_check: Vec<Socket> = vec![
(
mail_domain.clone(),
secure_pop_port,
ConnectionSecurity::Tls,
),
(pop_domain.clone(), secure_pop_port, ConnectionSecurity::Tls),
(mail_domain.clone(), pop_port, ConnectionSecurity::Plain),
(pop_domain, pop_port, ConnectionSecurity::Plain),
let tuples = vec![
(&mail_domain, secure_pop_port, ConnectionSecurity::Tls),
(&pop_domain, secure_pop_port, ConnectionSecurity::Tls),
(&mail_domain, pop_port, ConnectionSecurity::Plain),
(&pop_domain, pop_port, ConnectionSecurity::Plain),
];
let check_pop_sockets = spawn(check_sockets(sockets_to_check, &ServerConfigType::Pop));
for tuple in tuples {
let socket = Socket::from_tuple(tuple);
check_socket_threads.push(check_pop_sockets);
sockets_to_check.push(socket);
}
}
#[cfg(feature = "smtp")]
@ -163,56 +128,33 @@ pub async fn from_email(email_address: &str) -> Result<Config> {
let secure_smpt_port: u16 = 587;
let smpt_port: u16 = 25;
let smpt_domain = format!("smtp.{}", domain);
let sockets_to_check: Vec<Socket> = vec![
(
mail_domain.clone(),
secure_smpt_port,
ConnectionSecurity::StartTls,
),
(
smpt_domain.clone(),
secure_smpt_port,
ConnectionSecurity::StartTls,
),
(mail_domain, smpt_port, ConnectionSecurity::Plain),
(smpt_domain, smpt_port, ConnectionSecurity::Plain),
let smtp_domain = format!("smtp.{}", domain);
let tuples = vec![
(&mail_domain, secure_smpt_port, ConnectionSecurity::StartTls),
(&smtp_domain, secure_smpt_port, ConnectionSecurity::StartTls),
(&mail_domain, smpt_port, ConnectionSecurity::Plain),
(&smtp_domain, smpt_port, ConnectionSecurity::Plain),
];
let check_smtp_sockets =
spawn(check_sockets(sockets_to_check, &ServerConfigType::Smtp));
for tuple in tuples {
let socket = Socket::from_tuple(tuple);
check_socket_threads.push(check_smtp_sockets);
sockets_to_check.push(socket);
}
}
let check_sockets_results = join_all(check_socket_threads).await;
for check_sockets_result in check_sockets_results {
let working_sockets = check_sockets_result.unwrap();
if working_sockets.len() > 0 {
let mut working_sockets = working_sockets.into_iter();
let (socket_to_use, client_type) = match working_sockets.next() {
Some(socket) => socket,
// We know the array is larger than 0 items
None => unreachable!(),
};
let config: ServerConfig = ServerConfig::new(
client_type.clone(),
socket_to_use.1,
socket_to_use.0,
socket_to_use.2,
vec![AuthenticationType::ClearText],
);
if config.r#type() == &ServerConfigType::Smtp {
outgoing_configs.push(config);
} else {
incoming_configs.push(config);
}
let results = check_sockets(sockets_to_check).await?;
for (config_type, socket) in results {
let is_outgoing = config_type.is_outgoing();
let config = socket.into_server_config(config_type);
if is_outgoing {
outgoing_configs.push(config);
} else {
incoming_configs.push(config);
}
}
@ -238,7 +180,7 @@ pub async fn from_email(email_address: &str) -> Result<Config> {
mod test {
#[tokio::test]
async fn from_email() {
let email = "mail@samtaen.nl";
let email = "guusvanmeerveld@yahoo.com";
let config = super::from_email(email).await.unwrap();

@ -1,139 +1,210 @@
use std::{
io::{Read, Write},
time::Duration,
use std::time::Duration;
use async_native_tls::TlsConnector;
use async_tcp::{types::ErrorKind as TcpErrorKind, Config as TcpConfig, TcpClient};
use futures::{future::select_ok, FutureExt};
use tokio::{
io::{AsyncRead, AsyncWrite},
net::ToSocketAddrs,
};
use crate::types::{ConnectionSecurity, Error, ErrorKind, Result};
use super::types::{ServerConfigType, Socket};
// TODO: Make all of these functions use a singular socket instead of each creating their own connection.
/// Checks if a given connection is connected to an SMTP supporting server.
#[cfg(feature = "smtp")]
async fn is_smtp<S: AsyncRead + AsyncWrite + Unpin>(mut client: TcpClient<S>) -> Result<bool> {
// Read greeting
let greeting = client.read_response().await?;
client.close().await?;
let greeting = greeting.trim().to_ascii_lowercase();
let is_smtp = greeting.contains("smtp") || greeting.contains("esmtp");
Ok(is_smtp)
}
/// Checks if a given connection is connected to an IMAP supporting server.
#[cfg(feature = "imap")]
use {bufstream::BufStream, std::io::BufRead};
async fn is_imap<S: AsyncRead + AsyncWrite + Unpin>(mut client: TcpClient<S>) -> Result<bool> {
// Read greeting
client.read_response().await?;
#[cfg(feature = "pop")]
use crate::parse::map_pop_error;
let response = client.send_command("A0001 CAPABILITY").await?;
use crate::{
tls::create_tls_connector,
types::{self, ConnectionSecurity},
utils::{create_tcp_stream, create_tls_stream},
};
client.close().await?;
use super::types::ServerConfigType;
let response = response.trim().to_ascii_lowercase();
const IMAP_GREETING: &str = "* ok";
let is_imap = response
.split(' ')
.find(|capability| capability == &"imap4rev1")
.is_some();
/// This function assumes the given stream is a freshly opened connection
fn detect_from_stream<S: Read + Write + BufRead>(
stream: &mut S,
) -> types::Result<Option<ServerConfigType>> {
let mut response = String::new();
Ok(is_imap)
}
stream
.read_line(&mut response)
.map_err(|e| types::Error::new(types::ErrorKind::ImapError, e.to_string()))?;
#[cfg(feature = "imap")]
async fn check_for_imap<T: ToSocketAddrs, S: AsRef<str>>(
security: &ConnectionSecurity,
addr: &T,
domain: S,
tcp_config: &Option<TcpConfig>,
) -> Result<ServerConfigType> {
let is_imap = match security {
ConnectionSecurity::Tls => {
let connection = async_tcp::connect(&addr, domain, tcp_config.clone()).await?;
is_imap(connection).await?
}
_ => {
let connection = async_tcp::connect_plain(&addr, tcp_config.clone()).await?;
response.make_ascii_lowercase();
is_imap(connection).await?
}
};
let config_type = if response.starts_with(IMAP_GREETING) {
Some(ServerConfigType::Imap)
} else if response.contains("imap") {
Some(ServerConfigType::Imap)
} else if response.contains("smtp") {
Some(ServerConfigType::Smtp)
if is_imap {
Ok(ServerConfigType::Imap)
} else {
None
Err(Error::new(
ErrorKind::ConfigNotFound,
"Given server is not an imap server",
))
}
}
#[cfg(feature = "smtp")]
async fn check_for_smtp<T: ToSocketAddrs, S: AsRef<str>>(
security: &ConnectionSecurity,
addr: &T,
domain: S,
tcp_config: &Option<TcpConfig>,
) -> Result<ServerConfigType> {
let is_smtp = match security {
ConnectionSecurity::Tls => {
let connection = async_tcp::connect(&addr, domain, tcp_config.clone()).await?;
is_smtp(connection).await?
}
_ => {
let connection = async_tcp::connect_plain(&addr, tcp_config.clone()).await?;
is_smtp(connection).await?
}
};
Ok(config_type)
if is_smtp {
Ok(ServerConfigType::Smtp)
} else {
Err(Error::new(
ErrorKind::ConfigNotFound,
"Given server is not an smtp server",
))
}
}
#[cfg(feature = "pop")]
async fn check_for_pop<T: ToSocketAddrs, S: AsRef<str>>(
security: &ConnectionSecurity,
addr: &T,
domain: S,
tcp_config: &Option<TcpConfig>,
) -> Result<ServerConfigType> {
let tcp_config = tcp_config.as_ref().cloned().unwrap_or_default();
let connection_timeout = Some(tcp_config.into_timeout());
// The async-pop package checks the greeting for us when we call the connect function, so we don't have to do it ourselves.
let is_pop = match security {
ConnectionSecurity::Tls => {
let tls = TlsConnector::new();
async_pop3::connect(&addr, domain.as_ref(), &tls, connection_timeout)
.await
.is_ok()
}
_ => async_pop3::connect_plain(&addr, connection_timeout)
.await
.is_ok(),
};
if is_pop {
Ok(ServerConfigType::Pop)
} else {
Err(Error::new(
ErrorKind::ConfigNotFound,
"Given server is not a pop server",
))
}
}
/// Fetch the service type from a given server address. e.g I have a server at 192.168.0.1:993, this function could tell me that it is an Imap server.
///
/// This function is needed because, for example we can never assume that a service running on port 993 is an Imap server, even though that would be the expected behavior.
pub fn from_server(
domain: &str,
port: &u16,
security: &ConnectionSecurity,
) -> types::Result<Option<ServerConfigType>> {
let addr = (domain, *port);
let connect_timeout = Some(Duration::from_millis(2500));
pub async fn detect_server_config(socket: &Socket) -> Result<Option<ServerConfigType>> {
let addr = socket.addr();
let domain = socket.domain();
#[cfg(feature = "imap")]
{
let is_imap = match security {
ConnectionSecurity::Tls => {
let tls_stream = create_tls_stream(addr, domain, connect_timeout)?;
let security = socket.security();
let mut bufstream = BufStream::new(tls_stream);
let connect_timeout = Duration::from_millis(5 * 1000);
detect_from_stream(&mut bufstream)? == Some(ServerConfigType::Imap)
}
_ => {
let tcp_stream = create_tcp_stream(addr, connect_timeout)?;
let tcp_config = Some(TcpConfig::new(connect_timeout, true));
let mut bufstream = BufStream::new(tcp_stream);
let mut checkers = Vec::new();
detect_from_stream(&mut bufstream)? == Some(ServerConfigType::Imap)
}
};
#[cfg(feature = "imap")]
{
let check_for_imap_future = check_for_imap(security, &addr, domain, &tcp_config);
if is_imap {
return Ok(Some(ServerConfigType::Imap));
}
checkers.push(check_for_imap_future.boxed());
}
#[cfg(feature = "pop")]
{
let is_pop = match security {
ConnectionSecurity::Tls => {
let tls = create_tls_connector()?;
pop3::connect(addr, domain, &tls, connect_timeout)
.map_err(map_pop_error)
.is_ok()
}
_ => pop3::connect_plain(addr, connect_timeout)
.map_err(map_pop_error)
.is_ok(),
};
if is_pop {
return Ok(Some(ServerConfigType::Pop));
}
let check_for_smtp_future = check_for_pop(security, &addr, domain, &tcp_config);
checkers.push(check_for_smtp_future.boxed());
}
#[cfg(feature = "smtp")]
{
let is_smtp = match security {
ConnectionSecurity::Tls => {
let tls_stream = create_tls_stream(addr, domain, connect_timeout)?;
let mut bufstream = BufStream::new(tls_stream);
detect_from_stream(&mut bufstream)? == Some(ServerConfigType::Smtp)
}
_ => {
let tcp_stream = create_tcp_stream(addr, connect_timeout)?;
let check_for_smtp_future = check_for_smtp(security, &addr, domain, &tcp_config);
let mut bufstream = BufStream::new(tcp_stream);
detect_from_stream(&mut bufstream)? == Some(ServerConfigType::Smtp)
}
};
if is_smtp {
return Ok(Some(ServerConfigType::Smtp));
}
checkers.push(check_for_smtp_future.boxed());
}
Ok(None)
let result = select_ok(checkers).await;
match result {
Ok((config_type, _remaining)) => Ok(Some(config_type)),
Err(err) => match err.kind() {
ErrorKind::Tcp(tcp_err) => match tcp_err.kind() {
TcpErrorKind::Timeout(_) => Ok(None),
_ => Err(err),
},
_ => Err(err),
},
}
}
#[cfg(test)]
mod test {
use crate::{detect::types::ServerConfigType, types::ConnectionSecurity};
use crate::{
detect::{types::ServerConfigType, Socket},
types::ConnectionSecurity,
};
use super::from_server;
use super::detect_server_config;
#[test]
fn client_type() {
let domain = "outlook.office365.com";
#[tokio::test]
async fn client_type() {
let domain = "mail.samtaen.nl";
let imap_port = 993;
let smtp_port = 587;
let pop_port = 995;
@ -141,12 +212,16 @@ mod test {
#[cfg(feature = "imap")]
{
assert_eq!(
from_server(domain, &imap_port, &ConnectionSecurity::Tls).unwrap(),
detect_server_config(&Socket::new(domain, imap_port, ConnectionSecurity::Tls))
.await
.unwrap(),
Some(ServerConfigType::Imap),
);
assert_ne!(
from_server(domain, &pop_port, &ConnectionSecurity::Tls).unwrap(),
detect_server_config(&Socket::new(domain, pop_port, ConnectionSecurity::Tls))
.await
.unwrap(),
Some(ServerConfigType::Imap),
);
}
@ -154,25 +229,37 @@ mod test {
#[cfg(feature = "pop")]
{
assert_eq!(
from_server(domain, &pop_port, &ConnectionSecurity::Tls).unwrap(),
detect_server_config(&Socket::new(domain, pop_port, ConnectionSecurity::Tls))
.await
.unwrap(),
Some(ServerConfigType::Pop),
);
assert_ne!(
from_server(domain, &imap_port, &ConnectionSecurity::Tls).unwrap(),
detect_server_config(&Socket::new(domain, imap_port, ConnectionSecurity::Tls))
.await
.unwrap(),
Some(ServerConfigType::Pop),
);
}
#[cfg(feature = "imap")]
#[cfg(feature = "smtp")]
{
assert_eq!(
from_server(domain, &smtp_port, &ConnectionSecurity::StartTls).unwrap(),
detect_server_config(&Socket::new(
domain,
smtp_port,
ConnectionSecurity::StartTls
))
.await
.unwrap(),
Some(ServerConfigType::Smtp),
);
assert_ne!(
from_server(domain, &imap_port, &ConnectionSecurity::Tls).unwrap(),
detect_server_config(&Socket::new(domain, imap_port, ConnectionSecurity::Tls))
.await
.unwrap(),
Some(ServerConfigType::Smtp),
);
}

@ -1,11 +1,12 @@
use serde::Serialize;
use tokio::net::ToSocketAddrs;
use crate::{
parse::to_json,
types::{ConnectionSecurity, Result},
};
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
#[derive(Debug, Serialize, Clone, PartialEq, Eq, Hash)]
pub enum ServerConfigType {
Imap,
Pop,
@ -13,6 +14,15 @@ pub enum ServerConfigType {
Exchange,
}
impl ServerConfigType {
pub fn is_outgoing(&self) -> bool {
match self {
Self::Smtp => true,
_ => false,
}
}
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ServerConfig {
@ -164,4 +174,44 @@ impl Config {
}
}