diff --git a/Cargo.lock b/Cargo.lock index a37bf5f..86404be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -662,6 +662,22 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64 0.21.0", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "ena" version = "0.14.2" @@ -825,8 +841,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", + "futures-io", "futures-macro", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1186,6 +1204,29 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lettre" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d" +dependencies = [ + "base64 0.21.0", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna 0.3.0", + "mime", + "native-tls", + "nom", + "once_cell", + "quoted_printable", + "socket2", + "tokio", +] + [[package]] name = "libc" version = "0.2.141" @@ -1588,6 +1629,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24039f627d8285853cc90dcddf8c1ebfaa91f834566948872b225b9a28ed1b6" + [[package]] name = "radix_trie" version = "0.2.1" @@ -1965,6 +2012,7 @@ version = "0.1.0" dependencies = [ "actix-web", "chrono", + "lettre", "once_cell", "rand 0.8.5", "sequoia-net", diff --git a/Cargo.toml b/Cargo.toml index 44b28e7..f9548f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] actix-web = "4.3.1" chrono = "0.4.24" +lettre = "0.10.4" once_cell = "1.17.1" rand = "0.8.5" sequoia-net = "0.27.0" diff --git a/wkd.toml.example b/example.wkd.toml similarity index 69% rename from wkd.toml.example rename to example.wkd.toml index f4d10dc..0c46953 100644 --- a/wkd.toml.example +++ b/example.wkd.toml @@ -1,16 +1,15 @@ variant = "Advanced" +root_folder = "data" max_age = 900 cleanup_interval = 21600 port = 8080 +external_url = "http://localhost:8080" -[folder_structure] -root_folder = "data" -pending_folder = "pending" - -[smtp_settings] +[mail_settings] smtp_host = "mail.example.org" smtp_username = "keyservice" smtp_password = "verysecurepassword" smtp_port = 465 +smtp_tls = "Tls" mail_from = "key-submission@example.org" mail_subject = "Confirm this action" \ No newline at end of file diff --git a/src/confirmation.rs b/src/confirmation.rs index 0c308f3..11bf124 100644 --- a/src/confirmation.rs +++ b/src/confirmation.rs @@ -3,9 +3,12 @@ use chrono::Utc; use crate::errors::Error; use crate::management::{delete_key, Action, Pending}; use crate::pending_path; -use crate::settings::SETTINGS; +use crate::settings::{SMTPEncryption, SETTINGS}; use crate::utils::{get_email_from_cert, parse_pem}; +use crate::PENDING_FOLDER; +use lettre::transport::smtp::authentication::Credentials; +use lettre::{Message, SmtpTransport, Transport}; use std::fs; use std::path::Path; @@ -37,7 +40,7 @@ pub fn confirm_action(token: &str) -> Result<(), Error> { None => return Err(Error::ParseEmail), }; match sequoia_net::wkd::insert( - &SETTINGS.folder_structure.root_folder, + &SETTINGS.root_folder, domain, SETTINGS.variant, &cert, @@ -55,8 +58,42 @@ pub fn confirm_action(token: &str) -> Result<(), Error> { } } -pub fn send_confirmation_email(email: &str, action: &Action, token: &str) { - println!("Email sent to {email}"); - println!("Action: {action:?}"); - println!("Token: {token}"); +pub fn send_confirmation_email(email: &str, action: &Action, token: &str) -> Result<(), Error> { + println!("Sending mail, token: {}", &token); + let email = Message::builder() + .from(match SETTINGS.mail_settings.mail_from.parse() { + Ok(mailbox) => mailbox, + Err(_) => panic!("Unable to parse the email in the settings!"), + }) + .to(match email.parse() { + Ok(mailbox) => mailbox, + Err(_) => return Err(Error::ParseEmail), + }) + .subject(&SETTINGS.mail_settings.mail_subject) + .body(format!("{action} - {token}")); + let message = match email { + Ok(message) => message, + Err(_) => return Err(Error::MailGeneration), + }; + let creds = Credentials::new( + SETTINGS.mail_settings.smtp_username.to_owned(), + SETTINGS.mail_settings.smtp_password.to_owned(), + ); + let builder = match &SETTINGS.mail_settings.smtp_tls { + SMTPEncryption::Tls => SmtpTransport::relay(&SETTINGS.mail_settings.smtp_host), + SMTPEncryption::Starttls => { + SmtpTransport::starttls_relay(&SETTINGS.mail_settings.smtp_host) + } + }; + let mailer = match builder { + Ok(builder) => builder, + Err(_) => return Err(Error::SmtpBuilder), + } + .credentials(creds) + .port(SETTINGS.mail_settings.smtp_port) + .build(); + match mailer.send(&message) { + Ok(_) => Ok(()), + Err(_) => Err(Error::SendMail), + } } diff --git a/src/errors.rs b/src/errors.rs index 70c33ca..ca4732c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -3,26 +3,32 @@ use thiserror::Error; #[derive(Error, Debug, Clone, Copy)] pub enum Error { - #[error("Error while parsing cert")] + #[error("EP1: Error while parsing cert")] ParseCert, - #[error("Error while parsing an E-Mail address")] + #[error("EP2: Error while parsing an E-Mail address")] ParseEmail, - #[error("There is no pending request associated to this token")] + #[error("EM1: There is no pending request associated to this token")] MissingPending, - #[error("Requested key does not exist")] + #[error("EM2: Requested key does not exist")] MissingKey, - #[error("No E-Mail found in the certificate")] + #[error("EM3: No E-Mail found in the certificate")] MissingMail, - #[error("Error while serializing data")] + #[error("EE1: Error while sending the E-Mail")] + SendMail, + #[error("EE2: Error while building the SMTP connection")] + SmtpBuilder, + #[error("ES1: rror while serializing data")] SerializeData, - #[error("Error while deserializing data")] + #[error("ES2: Error while deserializing data")] DeserializeData, - #[error("The file is inaccessible")] + #[error("ES3: The file is inaccessible")] Inaccessible, - #[error("Error while adding a key to the wkd")] + #[error("ES4: Error while adding a key to the wkd")] AddingKey, - #[error("Error while generating the wkd path")] + #[error("EG1: Error while generating the wkd path")] PathGeneration, + #[error("EG2: Error while generating the email")] + MailGeneration, } impl actix_web::ResponseError for Error { diff --git a/src/main.rs b/src/main.rs index c30b2c1..7f0c724 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,8 @@ use std::fs; use std::path::Path; use tokio::{task, time}; +const PENDING_FOLDER: &str = "pending"; + #[derive(Deserialize, Debug)] struct Pem { key: String, @@ -39,7 +41,9 @@ async fn main() -> std::io::Result<()> { let mut metronome = time::interval(time::Duration::from_secs(SETTINGS.cleanup_interval)); loop { metronome.tick().await; - clean_stale(SETTINGS.max_age).unwrap(); + if clean_stale(SETTINGS.max_age).is_err() { + eprintln!("Error while cleaning stale requests..."); + } } }); HttpServer::new(|| App::new().service(submit).service(confirm).service(delete)) @@ -54,7 +58,7 @@ async fn submit(pem: web::Form) -> Result { let email = get_email_from_cert(&cert)?; let token = gen_random_token(); store_pending_addition(pem.key.clone(), &token)?; - send_confirmation_email(&email, &Action::Add, &token); + send_confirmation_email(&email, &Action::Add, &token)?; Ok(String::from("Key submitted successfully!")) } @@ -69,6 +73,6 @@ async fn delete(email: web::Path) -> Result { key_exists(&email.address)?; let token = gen_random_token(); store_pending_deletion(email.address.clone(), &token)?; - send_confirmation_email(&email.address, &Action::Delete, &token); + send_confirmation_email(&email.address, &Action::Delete, &token)?; Ok(String::from("Deletion request submitted successfully!")) } diff --git a/src/management.rs b/src/management.rs index 676dcd9..6158774 100644 --- a/src/management.rs +++ b/src/management.rs @@ -2,10 +2,11 @@ use crate::errors::Error; use crate::pending_path; use crate::settings::SETTINGS; use crate::utils::get_user_file_path; +use crate::PENDING_FOLDER; use chrono::Utc; use serde::{Deserialize, Serialize}; -use std::{fs, path::Path}; +use std::{fmt::Display, fs, path::Path}; #[derive(Serialize, Deserialize, Debug)] pub enum Action { @@ -13,6 +14,12 @@ pub enum Action { Delete, } +impl Display for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct Pending { action: Action, @@ -95,7 +102,7 @@ pub fn clean_stale(max_age: i64) -> Result<(), Error> { } pub fn delete_key(email: &str) -> Result<(), Error> { - let path = Path::new(&SETTINGS.folder_structure.root_folder).join(get_user_file_path(email)?); + let path = Path::new(&SETTINGS.root_folder).join(get_user_file_path(email)?); match fs::remove_file(path) { Ok(_) => Ok(()), Err(_) => Err(Error::Inaccessible), diff --git a/src/settings.rs b/src/settings.rs index 6e3510a..8b31c04 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,24 +1,18 @@ -use std::fs; - use once_cell::sync::Lazy; use sequoia_net::wkd::Variant; use serde::{Deserialize, Serialize}; +use std::fs; #[derive(Serialize, Deserialize, Debug)] pub struct Settings { #[serde(with = "VariantDef")] pub variant: Variant, + pub root_folder: String, pub max_age: i64, pub cleanup_interval: u64, pub port: u16, - pub folder_structure: FolderStructure, - pub smtp_settings: MailSettings, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct FolderStructure { - pub root_folder: String, - pub pending_folder: String, + pub external_url: String, + pub mail_settings: MailSettings, } #[derive(Serialize, Deserialize, Debug)] @@ -27,6 +21,7 @@ pub struct MailSettings { pub smtp_username: String, pub smtp_password: String, pub smtp_port: u16, + pub smtp_tls: SMTPEncryption, pub mail_from: String, pub mail_subject: String, } @@ -38,10 +33,22 @@ pub enum VariantDef { Direct, } +#[derive(Serialize, Deserialize, Debug)] +pub enum SMTPEncryption { + Tls, + Starttls, +} + fn get_settings() -> Settings { println!("Reaing settings..."); - let content = fs::read_to_string("wkd.toml").unwrap(); - toml::from_str(&content).unwrap() + let content = match fs::read_to_string("wkd.toml") { + Ok(content) => content, + Err(_) => panic!("Unable to access settings file!"), + }; + match toml::from_str(&content) { + Ok(settings) => settings, + Err(_) => panic!("Unable to parse settings from file!"), + } } pub static SETTINGS: Lazy = Lazy::new(get_settings); diff --git a/src/utils.rs b/src/utils.rs index f71a9ea..1527a31 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,8 +9,7 @@ use std::path::{Path, PathBuf}; #[macro_export] macro_rules! pending_path { () => { - Path::new(&SETTINGS.folder_structure.root_folder) - .join(&SETTINGS.folder_structure.pending_folder) + Path::new(&SETTINGS.root_folder).join(PENDING_FOLDER) }; } @@ -54,7 +53,7 @@ pub fn get_user_file_path(email: &str) -> Result { pub fn key_exists(email: &str) -> Result { let path = get_user_file_path(email)?; - if !pending_path!().join(path).is_file() { + if !Path::new(&SETTINGS.root_folder).join(path).is_file() { return Err(Error::MissingKey); } Ok(true)