wedding-rsvp-rs/src/main.rs

388 lines
13 KiB
Rust

#![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<Self, Self::Error> {
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<String> {
// 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<Settings>) -> 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 = "<user_input>")]
fn submit_task (host: HostHeader,
settings: State<Settings>,
user_input: Option<Form<OwnedUserInput>>,
client_addr: &ClientRealAddr,
ip_epoch: State<Mutex<HashMap<String, u64>>>) -> 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("<br />Emailaddress: ");
rsvp_message.push_str(&x.email);
rsvp_message.push_str("<br />Guests: ");
rsvp_message.push_str(&x.guests.to_string());
rsvp_message.push_str("<br />Message:<br /><br />");
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("<br />Error message:<br />");
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/<name>")]
fn thanks(name: String, host:HostHeader, settings: State<Settings>) -> 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<Settings>) -> 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<Settings>) -> 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<Settings>) -> 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<HashMap<String, u64>> = 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();
}