#![feature(proc_macro_hygiene, decl_macro)] #[macro_use] extern crate rocket; #[macro_use] extern crate serde_derive; extern crate rocket_contrib; extern crate lettre; extern crate lettre_email; extern crate regex; extern crate urlencoding; extern crate config; extern crate strfmt; extern crate rocket_client_addr; use rocket_client_addr::ClientRealAddr; use strfmt::strfmt; use rocket::Request; use rocket::State; use rocket::outcome::Outcome; use rocket_contrib::serve::StaticFiles; use rocket::response::Redirect; use rocket_contrib::templates::Template; use rocket::request::{Form, FromRequest}; use lettre::smtp::authentication::{Credentials, Mechanism}; use lettre_email::{Email, mime::TEXT_PLAIN}; use lettre::{Transport, SmtpClient}; use lettre::smtp::extension::ClientId; use std::collections::HashMap; use std::sync::Mutex; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; use regex::Regex; pub mod settings; use settings::{Settings, LanguageStrings}; ////////////////////////// // Structs ////////////////////////// #[derive(FromForm)] struct OwnedUserInput { name: String, email: String, guests: u8, message: String } pub struct HostHeader<'a>(pub &'a str); impl<'a, 'r> FromRequest<'a, 'r> for HostHeader<'a> { type Error = (); fn from_request(request: &'a Request) -> rocket::request::Outcome { match request.headers().get_one("Host") { Some(h) => Outcome::Success(HostHeader(h)), None => Outcome::Forward(()), } } } ////////////////////////// // Functions ////////////////////////// fn lang_strings<'a>(host: HostHeader, settings: &'a Settings) -> &'a LanguageStrings { // Grab TLD and grab the mapping from the settings. Check with a match construct // whether there is a TLD (corner case: running the site in development without // a valid TLD). let lang = match strip_tld(&host.0) { Some(tld) => { // If the TLD is valid, try to get the mapping to a language let tld_mapping = settings.tld_mapping.get(&tld); // See if this is a valid mapping, otherwise, fall back to the default let lang = match tld_mapping { Some(lang) => lang, None => &settings.default_lang, }; // Return to variable lang }, // Return default language None => &settings.default_lang }; // Grab actual language strings and return &settings.languages.get(lang).unwrap() } fn validate_name(name: &String) -> bool { // Check if the name only consists of letters and numbers // This is a placeholder and currently simply returns true if name.len() > 0 { true } else { false } } fn validate_email(email: &String) -> bool { // Check if the emailaddress is valid // This function is not completely correct: emailregex.com Regex::new(r"^[a-zA-Z0-9\.\-_]+@[a-zA-Z0-9\-\.]+\.[a-zA-Z]+$").unwrap().is_match(email) } fn strip_tld(url: &str) -> Option { // Get the top level domain which is used to determine the language let re = Regex::new(r"^.*\.([a-z]+)$").unwrap(); let tld = match re.captures(url) { Some(tld) => Some(tld[1].to_string()), None => None }; tld } fn send_email(to_name: &String, to_address: &String, subject: &String, message: &String, attachment: Option<&String>, settings: &State) -> Result<(), String> { // Load HJSON config let email_credentials = &settings.email_credentials; // Translate config into more readable variables let smtp_server = email_credentials.smtp_server.to_string(); let smtp_name = email_credentials.smtp_name.to_string(); let smtp_username = email_credentials.smtp_username.to_string(); let smtp_password = email_credentials.smtp_password.to_string(); // If an attachment is defined, send it. let email = match attachment { Some(x) => { Email::builder() .to((to_address, to_name)) .from((&smtp_username, &smtp_name)) .subject(subject) .html(message) .attachment_from_file(Path::new(x), None, &TEXT_PLAIN) .unwrap() .build() .unwrap() }, None => { Email::builder() .to((to_address, to_name)) .from((&smtp_username, &smtp_name)) .subject(subject) .html(message) .build() .unwrap() } }; // Connect to a remote server on a custom port let mut mailer = SmtpClient::new_simple(&smtp_server).unwrap() // Set the name sent during EHLO/HELO, default is `localhost` .hello_name(ClientId::Domain(smtp_server)) // Add credentials for authentication .credentials(Credentials::new(smtp_username, smtp_password)) // Enable SMTPUTF8 if the server supports it .smtp_utf8(true) // Configure expected authentication mechanism .authentication_mechanism(Mechanism::Plain).transport(); match mailer.send(email.into()) { Ok(_x) => { mailer.close(); return Ok(()); }, Err(e) => { mailer.close(); return Err(e.to_string()); } }; } ////////////////////////// // Pages of website ////////////////////////// #[post("/", data = "")] fn submit_task (host: HostHeader, settings: State, user_input: Option>, client_addr: &ClientRealAddr, ip_epoch: State>>) -> Redirect { // Create struct with all language strings let language_strings = lang_strings(host, &settings); let result : Result<(), String>; match user_input { Some(x) => { // Check input for anything strange or something that does not look like // an email address. if validate_name(&x.name) && validate_email(&x.email) { // Create HashMap to use with strfnt let mut vars = HashMap::new(); vars.insert("name".to_string(), &x.name); // Check hashtable to make sure this IP doesn't try to spam. let epoch = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_secs(); // Unlock mutex on ip->epoch HashTable let mut ip_epoch_unlocked = ip_epoch.inner().lock().unwrap(); // Check how long ago the latest message was sent match ip_epoch_unlocked.get(&client_addr.get_ipv4_string().unwrap()) { Some(x) => { if epoch - x < settings.spam_wait { return Redirect::to("spam"); } else { // We are OK to send! ip_epoch_unlocked.insert(client_addr.get_ipv4_string().unwrap(), epoch); } } None => { // We are OK to send! ip_epoch_unlocked.insert(client_addr.get_ipv4_string().unwrap(), epoch); } }; if x.guests > 0 { // Send email about attendaning wedding result = send_email(&x.name, &x.email, &language_strings.attending_email.subject, &strfmt(&language_strings.attending_email.message, &vars).unwrap(), Some(&language_strings.attending_email.attachment), &settings); } else { // Send email about not attending wedding result = send_email(&x.name, &x.email, &language_strings.not_attending_email.subject, &strfmt(&language_strings.not_attending_email.message, &vars).unwrap(), None, &settings); } } else { result = Err("Validate e-mail or name went wrong.".to_string()); } // Send a message to the sender's e-mailaddres to inform about // a new attendent. let email_credentials = &settings.email_credentials; let smtp_name = email_credentials.smtp_name.to_string(); let smtp_username = email_credentials.smtp_username.to_string(); let mut rsvp_subject = String::new(); let mut rsvp_message = String::new(); rsvp_message.push_str("Name: "); rsvp_message.push_str(&x.name); rsvp_message.push_str("
Emailaddress: "); rsvp_message.push_str(&x.email); rsvp_message.push_str("
Guests: "); rsvp_message.push_str(&x.guests.to_string()); rsvp_message.push_str("
Message:

"); rsvp_message.push_str(&x.message); // Create subject and add error message (if necessary) let mut error = false; rsvp_subject.push_str(&x.name); match result { Ok(_x) => { rsvp_subject.push_str(" just responded to your event!"); }, Err(e) => { rsvp_subject.push_str(" just responded to your event! (ERROR)"); rsvp_message.push_str("
Error message:
"); rsvp_message.push_str(&e); error = true; } }; // Also check if e-mail to the organisation didn't go through! match send_email(&smtp_name, &smtp_username, &rsvp_subject, &rsvp_message, None, &settings) { Ok(_x) => {}, Err(_e) => { return Redirect::to("email-issue"); } }; // Redirect to thank you page or to error page if error { return Redirect::to("email-issue"); } else { return Redirect::to(format!("/thanks/{}", urlencoding::encode(&x.name))); } } None => Redirect::to("email-issue") } } #[get("/thanks/")] fn thanks(name: String, host:HostHeader, settings: State) -> Template { // Create struct with all language strings let language_strings = lang_strings(host, &settings).clone(); // Create HashMap to use with strfnt let mut vars = HashMap::new(); vars.insert("name".to_string(), name); // Create template with appropriate strings Template::render("message", &settings::MessageStrings{title : language_strings.thanks.title.clone(), h1 : strfmt(&language_strings.thanks.h1, &vars).unwrap(), message : language_strings.thanks.message.clone()}) } #[get("/email-issue")] fn email_issue(host:HostHeader, settings: State) -> Template { // Create struct with all language strings let language_strings = lang_strings(host, &settings).clone(); // Create template with appropriate strings Template::render("message", &language_strings.email_issue) } #[get("/spam")] fn spam(host:HostHeader, settings: State) -> Template { // Create struct with all language strings let language_strings = lang_strings(host, &settings).clone(); // Create template with appropriate strings Template::render("message", &language_strings.spam) } #[get("/")] fn index(host: HostHeader, settings: State) -> Template { // Create struct with all language strings let language_strings = lang_strings(host, &settings); // Parse index page Template::render("index", &language_strings.form) } #[catch(404)] fn not_found() -> Template { let h1 = String::from("Oops..! 404 :-("); let title = "404".to_string(); let message = "".to_string(); let context = settings::MessageStrings{title, h1, message}; Template::render("message", &context) } ////////////////////////// // Launch Rocket ////////////////////////// fn main() { let ip_epoch: Mutex> = Mutex::new(HashMap::new()); rocket::ignite() .manage(Settings::new().unwrap()) .manage(ip_epoch) .mount("/", routes![index]) .mount("/", routes![thanks]) .mount("/", routes![spam]) .mount("/", routes![email_issue]) .mount("/", routes![submit_task]) .mount("/css", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/css"))) .mount("/js", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/js"))) .mount("/vendor", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/vendor"))) .mount("/images", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/images"))) .mount("/fonts", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/fonts"))) .attach(Template::fairing()) .register(catchers![not_found]) .launch(); }