refactor(web): updated web to use async sdk

main
Guus van Meerveld 2 months ago
parent 4fc36d6474
commit 2871fe13ce
Signed by: Guusvanmeerveld
GPG Key ID: 2BA7D7912771966E

@ -18,12 +18,15 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["devtools", "isolation", "shell-open", "system-tray", "window-close", "window-create"] }
sdk = { path = "../../../packages/sdk", version = "0.1" }
chacha20poly1305 = { version = "0.10", features = ["stream"] }
open = "3.0"
base64 = "0.21"
directories = "4.0"
futures = "0.3"
dashmap = "5.4.0"
keyring = "2.0.2"
data-encoding = "2.3.3"
ring = "0.16.20"
whoami = "1.4.0"
[features]
# by default Tauri runs in production mode

@ -1,23 +0,0 @@
use base64::Engine;
use crate::types::{Error, ErrorKind, Result};
const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new(
&base64::alphabet::URL_SAFE,
base64::engine::general_purpose::NO_PAD,
);
/// Encode an array of bytes to a base64-encoded string
pub fn encode(data: &[u8]) -> String {
BASE64_ENGINE.encode(data)
}
/// Decode a base64-encoded array of bytes to an array of utf-8 encoded bytes
pub fn decode(data: &[u8]) -> Result<Vec<u8>> {
BASE64_ENGINE.decode(data).map_err(|e| {
Error::new(
ErrorKind::DecodeBase64,
format!("Failed to decode base64: {}", e),
)
})
}

@ -4,12 +4,9 @@ use sdk::{
types::{MailBox, Message, Preview},
};
use crate::{
files::CacheFile,
types::{session, Result, Sessions},
};
use crate::{identifier::Identifier, keyring, parse::to_json, sessions::Sessions, types::Result};
use tauri::{async_runtime::spawn_blocking, State};
use tauri::State;
#[tauri::command(async)]
pub async fn detect_config(email_address: String) -> Result<Config> {
@ -24,14 +21,20 @@ pub async fn login(
// Connect and login to the mail servers using the user provided credentials.
let mail_sessions = sdk::session::create_sessions(&credentials).await?;
let generate_token = spawn_blocking(move || session::generate_token(&credentials));
let mut identifier = Identifier::from(&credentials);
identifier.hash()?;
let identifier: String = identifier.into();
let credentials_json = to_json(&credentials)?;
let token: String = generate_token.await.unwrap()?;
keyring::set(&identifier, credentials_json)?;
session_handler.insert_session(&token, mail_sessions)?;
session_handler.insert_session(&identifier, mail_sessions)?;
// Return the key and nonce to the frontend so it can verify its session later.
Ok(token)
Ok(identifier)
}
#[tauri::command(async)]
@ -39,15 +42,14 @@ pub async fn login(
pub async fn list(token: String, sessions: State<'_, Sessions>) -> Result<Vec<MailBox>> {
let session = sessions.get_incoming_session(&token).await?;
let fetch_box_list = spawn_blocking(move || {
let mut session_lock = session.lock().unwrap();
let mut session_lock = session.lock().await;
let list = session_lock.box_list().map(|box_list| box_list.clone())?;
let list = session_lock
.box_list()
.await
.map(|box_list| box_list.clone())?;
Ok(list)
});
fetch_box_list.await.unwrap()
Ok(list)
}
#[tauri::command(async)]
@ -55,15 +57,14 @@ pub async fn list(token: String, sessions: State<'_, Sessions>) -> Result<Vec<Ma
pub async fn get(token: String, box_id: String, sessions: State<'_, Sessions>) -> Result<MailBox> {
let session = sessions.get_incoming_session(&token).await?;
let fetch_box = spawn_blocking(move || {
let mut session_lock = session.lock().unwrap();
let mailbox = session_lock.get(&box_id).map(|mailbox| mailbox.clone())?;
let mut session_lock = session.lock().await;
Ok(mailbox)
});
let mailbox = session_lock
.get(&box_id)
.await
.map(|mailbox| mailbox.clone())?;
fetch_box.await.unwrap()
Ok(mailbox)
}
#[tauri::command(async)]
@ -77,15 +78,11 @@ pub async fn messages(
) -> Result<Vec<Preview>> {
let session = sessions.get_incoming_session(&token).await?;
let fetch_message_list = spawn_blocking(move || {
let mut session_lock = session.lock().unwrap();
let message_list = session_lock.messages(&box_id, start, end)?;
let mut session_lock = session.lock().await;
Ok(message_list)
});
let message_list = session_lock.messages(&box_id, start, end).await?;
fetch_message_list.await.unwrap()
Ok(message_list)
}
#[tauri::command(async)]
@ -98,35 +95,21 @@ pub async fn get_message(
) -> Result<Message> {
let session = sessions.get_incoming_session(&token).await?;
let fetch_message = spawn_blocking(move || {
let mut session_lock = session.lock().unwrap();
let mut session_lock = session.lock().await;
let message = session_lock.get_message(&box_id, &message_id)?;
let message = session_lock.get_message(&box_id, &message_id).await?;
Ok(message)
});
fetch_message.await.unwrap()
Ok(message)
}
#[tauri::command(async)]
/// Log out of the currently logged in account.
pub async fn logout(token: String, sessions: State<'_, Sessions>) -> Result<()> {
let session = sessions.get_incoming_session(&token).await?;
let logout = spawn_blocking(move || {
let mut session_lock = session.lock().unwrap();
session_lock.logout()?;
let (_, nonce) = session::get_nonce_and_key_from_token(&token)?;
let cache = CacheFile::from_session_name(nonce);
pub async fn logout(identifier: String, sessions: State<'_, Sessions>) -> Result<()> {
let session = sessions.get_incoming_session(&identifier).await?;
cache.delete()?;
let mut session_lock = session.lock().await;
Ok(())
});
session_lock.logout().await?;
logout.await.unwrap()
Ok(())
}

@ -1,42 +0,0 @@
use chacha20poly1305::{
aead::{rand_core::RngCore, Aead, OsRng},
KeyInit, XChaCha20Poly1305,
};
use crate::types::{Error, ErrorKind, Result};
pub fn generate_key() -> [u8; 32] {
let mut key = [0u8; 32];
OsRng.fill_bytes(&mut key);
key
}
pub fn generate_nonce() -> [u8; 24] {
let mut nonce = [0u8; 24];
OsRng.fill_bytes(&mut nonce);
nonce
}
pub fn encrypt(data: &[u8], key: &[u8; 32], nonce: &[u8; 24]) -> Result<Vec<u8>> {
let cipher = XChaCha20Poly1305::new(key.into());
let encrypted = cipher
.encrypt(nonce.into(), data)
.map_err(|e| Error::new(ErrorKind::Crypto, format!("Failed to encrypt data: {}", e)))?;
Ok(encrypted)
}
pub fn decrypt(data: &[u8], key: &[u8], nonce: &[u8]) -> Result<Vec<u8>> {
let cipher = XChaCha20Poly1305::new(key.into());
let decrypted = cipher
.decrypt(nonce.into(), data)
.map_err(|e| Error::new(ErrorKind::Crypto, format!("Failed to decrypt data: {}", e)))?;
Ok(decrypted)
}

@ -1,72 +0,0 @@
use directories::ProjectDirs;
use std::{
fs::{self, File},
io::Read,
path::PathBuf,
};
use crate::types::{Error, ErrorKind, Result};
fn parse_io_error(error: std::io::Error) -> Error {
Error::new(ErrorKind::IoError, error.to_string())
}
const APP_NAME: &str = "Dust-Mail";
fn ensure_cache_dir(file_name: &str) -> Result<PathBuf> {
if let Some(project_dirs) =
ProjectDirs::from("dev.guusvanmeerveld", "Guus van Meerveld", APP_NAME)
{
let cache_dir = project_dirs.cache_dir().join(APP_NAME.to_ascii_lowercase());
if let Some(p) = cache_dir.parent() {
fs::create_dir_all(p).map_err(parse_io_error)?
};
return Ok(cache_dir.with_file_name(file_name));
};
Err(Error::new(
ErrorKind::NoCacheDir,
"Could locate a valid cache directory",
))
}
pub struct CacheFile(String);
impl CacheFile {
pub fn from_session_name<S: Into<String>>(session_name: S) -> Self {
Self(format!("{}.session", session_name.into()))
}
pub fn new<S: Into<String>>(file_name: S) -> Self {
Self(file_name.into())
}
/// Read a file with a given filename from the application's cache directory.
pub fn read(&self, buf: &mut Vec<u8>) -> Result<()> {
let cache_file = ensure_cache_dir(&self.0)?;
let mut file = File::open(cache_file).map_err(parse_io_error)?;
file.read_to_end(buf).map_err(parse_io_error)?;
Ok(())
}
pub fn delete(&self) -> Result<()> {
let cache_file = ensure_cache_dir(&self.0)?;
fs::remove_file(cache_file).map_err(parse_io_error)
}
/// Write a file with a given filename to the applications cache directory.
pub fn write(&self, data: &[u8]) -> Result<()> {
let cache_file = ensure_cache_dir(&self.0)?;
fs::write(cache_file, data).map_err(parse_io_error)?;
Ok(())
}
}

@ -0,0 +1,31 @@
use std::io::Read;
use crate::types::Result;
use data_encoding::HEXUPPER;
use ring::digest::{Context, Digest, SHA256};
fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest> {
let mut context = Context::new(&SHA256);
let mut buffer = [0; 1024];
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
context.update(&buffer[..count]);
}
Ok(context.finish())
}
pub fn sha256_hex<R: Read>(reader: R) -> Result<String> {
let digest = sha256_digest(reader)?;
let hex = HEXUPPER.encode(digest.as_ref());
Ok(hex)
}

@ -0,0 +1,47 @@
use sdk::session::{FullLoginOptions, LoginType};
use crate::{hash::sha256_hex, types::Result};
pub struct Identifier {
id: String,
}
impl Identifier {
/// Hash the currently stored identifier
pub fn hash(&mut self) -> Result<()> {
self.id = sha256_hex(self.id.as_bytes())?;
Ok(())
}
}
impl Into<String> for Identifier {
fn into(self) -> String {
self.id
}
}
impl From<&FullLoginOptions> for Identifier {
fn from(login_options: &FullLoginOptions) -> Self {
// TODO: Outgoing support
let incoming = login_options.incoming_options();
let (username, password) = match incoming.login_type() {
LoginType::PasswordBased(creds) => (creds.username(), creds.password()),
LoginType::OAuthBased(creds) => (creds.username(), creds.access_token()),
};
// A string that is unique to these login options.
let credentials_string = format!(
"{}:{}@{}:{}",
username,
password,
incoming.domain(),
incoming.port()
);
Self {
id: credentials_string,
}
}
}

@ -0,0 +1,31 @@
use crate::types::Result;
use keyring::Entry;
const APPLICATION_NAME: &str = "Dust-Mail";
fn build_entry_from_identifier<T: AsRef<str>>(identifier: T) -> Result<Entry> {
let username = whoami::username();
let name = format!("{}:{}", APPLICATION_NAME, identifier.as_ref());
let entry = Entry::new(&name, &username)?;
Ok(entry)
}
pub fn get<T: AsRef<str>>(identifier: T) -> Result<String> {
let entry = build_entry_from_identifier(identifier)?;
let password = entry.get_password()?;
Ok(password)
}
pub fn set<T: AsRef<str>, S: AsRef<str>>(identifier: T, value: S) -> Result<()> {
let entry = build_entry_from_identifier(identifier)?;
entry.set_password(value.as_ref())?;
Ok(())
}

@ -3,18 +3,19 @@
windows_subsystem = "windows"
)]
mod base64;
mod commands;
mod cryptography;
mod files;
mod hash;
mod keyring;
mod sessions;
mod identifier;
mod menu;
mod parse;
mod tray;
mod types;
use sessions::Sessions;
use tauri::{Manager, SystemTrayEvent};
use types::Sessions;
#[derive(Clone, serde::Serialize)]
struct Payload {

@ -2,17 +2,10 @@ use serde::Serialize;
use serde_json;
use crate::types::{Error, ErrorKind, Result};
use crate::types::Result;
pub fn parse_sdk_error(error: sdk::types::Error) -> Error {
Error::new(ErrorKind::MailError(error), "")
}
pub fn to_json<T: Serialize + ?Sized>(data: &T) -> Result<String> {
let serialized = serde_json::to_string(data)?;
pub fn to_json<T: Serialize + ?Sized>(data: &T) -> Result<Vec<u8>> {
serde_json::to_vec(data).map_err(|e| {
Error::new(
ErrorKind::SerializeJSON,
format!("Failed to serialize data to JSON: {}", e),
)
})
Ok(serialized)
}

@ -0,0 +1,64 @@
use dashmap::DashMap;
use serde_json::from_str;
use std::sync::Arc;
use sdk::session::{MailSessions, ThreadSafeIncomingSession};
use crate::{keyring, types::Result};
pub struct Sessions {
sessions_map: DashMap<String, Arc<MailSessions>>,
}
impl Sessions {
pub fn new() -> Self {
Self {
sessions_map: DashMap::new(),
}
}
pub fn insert_session<S: Into<String>>(
&self,
identifier: S,
sessions: MailSessions,
) -> Result<()> {
let identifier = identifier.into();
self.sessions_map.insert(identifier, Arc::new(sessions));
Ok(())
}
pub async fn get_incoming_session<S: AsRef<str>>(
&self,
identifier: S,
) -> Result<ThreadSafeIncomingSession> {
let mail_sessions = self.get_session(identifier.as_ref()).await?;
Ok(mail_sessions.incoming().clone())
}
pub async fn get_session<S: Into<String>>(&self, identifier: S) -> Result<Arc<MailSessions>> {
let identifier = identifier.into();
match self.sessions_map.get(&identifier) {
Some(sessions) => Ok(sessions.clone()),
None => {
// If we don't have a session stored, we try to get it from the credentials stored in the keyring.
let credentials_json = keyring::get(&identifier)?;
let credentials = from_str(&credentials_json)?;
let mail_sessions = sdk::session::create_sessions(&credentials).await?;
self.insert_session(identifier.clone(), mail_sessions)?;
match self.sessions_map.get(&identifier) {
Some(sessions) => Ok(sessions.clone()),
None => unreachable!(),
}
}
}
}
}

@ -1,14 +1,19 @@
use std::result;
use std::{
error::{self, Error as StdError},
fmt,
io::Error as IoError,
result,
};
// pub mod credentials;
pub mod session;
pub use session::Sessions;
// pub use credentials::Credentials;
use serde::Serialize;
use keyring::Error as KeyringError;
use sdk::types::Error as SdkError;
use serde_json::Error as JsonError;
use serde::{ser::SerializeStruct, Serialize};
#[derive(Serialize)]
#[derive(Debug)]
pub struct Error {
message: String,
kind: ErrorKind,
@ -21,29 +26,84 @@ impl Error {
kind,
}
}
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
}
impl From<sdk::types::Error> for Error {
fn from(sdk_error: sdk::types::Error) -> Self {
impl From<SdkError> for Error {
fn from(sdk_error: SdkError) -> Self {
Error::new(
ErrorKind::MailError(sdk_error),
ErrorKind::Mail(sdk_error),
"Error with upstream mail server",
)
}
}
#[derive(Serialize)]
impl From<KeyringError> for Error {
fn from(keyring_error: KeyringError) -> Self {
Error::new(ErrorKind::Keyring(keyring_error), "Error with keyring")
}
}
impl From<JsonError> for Error {
fn from(json_error: JsonError) -> Self {
Error::new(
ErrorKind::Json(json_error),
"Failed to serialize/deserialize json data",
)
}
}
impl From<IoError> for Error {
fn from(io_error: IoError) -> Self {
Error::new(ErrorKind::Io(io_error), "IO error")
}
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let source = self.source().unwrap_or(&self);
let mut state = serializer.serialize_struct("Error", 2)?;
state.serialize_field("message", &source.to_string())?;
state.serialize_field("kind", "MailError")?;
state.end()
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self.kind() {
ErrorKind::Io(e) => e.source(),
ErrorKind::Json(e) => e.source(),
ErrorKind::Keyring(e) => e.source(),
ErrorKind::Mail(e) => e.source(),
_ => None,
}
}
fn description(&self) -> &str {
&self.message
}
}
#[derive(Debug)]
pub enum ErrorKind {
MailError(sdk::types::Error),
IoError,
NoCacheDir,
InvalidInput,
NotLoggedIn,
DecodeBase64,
Crypto,
InvalidToken,
SerializeJSON,
DeserializeJSON,
Mail(SdkError),
Io(IoError),
Keyring(KeyringError),
Json(JsonError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
pub type Result<T> = result::Result<T, Error>;

@ -1,62 +0,0 @@
mod utils;
use dashmap::DashMap;
pub use utils::{generate_token, get_nonce_and_key_from_token};
use std::sync::Arc;
use sdk::session::{MailSessions, create_sessions, ThreadSafeIncomingSession};
use crate::types::{Error, ErrorKind, Result};
pub struct Sessions(DashMap<String, Arc<MailSessions>>);
impl Sessions {
pub fn new() -> Self {
Self(DashMap::new())
}
pub fn insert_session(&self, token: &str, session: MailSessions) -> Result<()> {
let (_, nonce_base64) = get_nonce_and_key_from_token(token)?;
let key = format!("{}-incoming", nonce_base64);
let thread_safe_session = Arc::new(session);
self.0.insert(key, thread_safe_session);
Ok(())
}
pub async fn get_incoming_session(&self, token: &str) -> Result<ThreadSafeIncomingSession> {
let mail_sessions = self.get_sessions(token).await?;
Ok(mail_sessions.incoming())
}
pub async fn get_sessions(&self, token: &str) -> Result<Arc<MailSessions>> {
let (_, nonce_base64) = get_nonce_and_key_from_token(token)?;
let key = format!("{}-incoming", nonce_base64);
match self.0.get(&key) {
// Return the current session
Some(session) => Ok(session.clone()),
None => {
let credentials = utils::get_credentials(token)?;
let mail_sessions = create_sessions(&credentials).await?;
self.0.insert(key.clone(), Arc::new(mail_sessions));
match self.0.get(&key) {
Some(session) => Ok(session.clone()),
None => Err(Error::new(
ErrorKind::NotLoggedIn,
"Could not find incoming session",
)),
}
}
}
}
}

@ -1,93 +0,0 @@
use sdk::session::FullLoginOptions;
use crate::{
base64, cryptography,
files::CacheFile,
parse,
types::{Error, ErrorKind, Result},
};
/// Given a token, get the encryption nonce and key.
pub fn get_nonce_and_key_from_token(token: &str) -> Result<(String, String)> {
let mut split = token.split(':');
let nonce_base64 = match split.next() {
Some(key) => key.to_string(),
None => {
return Err(Error::new(
ErrorKind::InvalidToken,
"Token is missing a nonce",
))
}
};
let key_base64 = match split.next() {
Some(key) => key.to_string(),
None => {
return Err(Error::new(
ErrorKind::InvalidToken,
"Token is missing a key",
))
}
};
Ok((key_base64, nonce_base64))
}
/// Given a token, find the corresponding encrypted login credentials in the cache dir and return them.
pub fn get_credentials(token: &str) -> Result<FullLoginOptions> {
let (key_base64, nonce_base64) = get_nonce_and_key_from_token(token)?;
let key = base64::decode(key_base64.as_bytes())?;
let nonce = base64::decode(nonce_base64.as_bytes())?;
let mut encrypted: Vec<u8> = Vec::new();
let cache = CacheFile::from_session_name(nonce_base64);
match cache.read(&mut encrypted) {
Ok(_) => {}
Err(_) => {
return Err(Error::new(
ErrorKind::NotLoggedIn,
"Could not find login credentials, please login",
));
}
};
let decrypted = cryptography::decrypt(&encrypted, &key, &nonce)?;
let login_options: FullLoginOptions = serde_json::from_slice(&decrypted).map_err(|e| {
Error::new(
ErrorKind::DeserializeJSON,
format!("Could not deserialize encrypted login info {}", e),
)
})?;
Ok(login_options)
}
pub fn generate_token(credentials: &FullLoginOptions) -> Result<String> {
// Serialize the given options to json
let options_json = parse::to_json(credentials)?;
// Generate a key and nonce to sign the options
let key = cryptography::generate_key();
let nonce = cryptography::generate_nonce();
// Crypographically sign the login options.
let encrypted = cryptography::encrypt(&options_json, &key, &nonce)?;
// Convert the key and nonce to base64
let nonce_base64 = base64::encode(&nonce);
let key_base64 = base64::encode(&key);
let cache = CacheFile::from_session_name(&nonce_base64);
// Write to file cache so the credentials are still available when the program restarts.
cache.write(&encrypted)?;
let token: String = format!("{}:{}", nonce_base64, key_base64);
Ok(token)
}
Loading…
Cancel
Save