388 lines
13 KiB
Rust
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();
|
|
}
|