First commit of wedding-rsvp-rs
This commit includes a fully functional template and Rust backend. The website is able to send e-mails, displays strings in multiple languages, include attachments, and has basic protection again spam (IP based).
This commit is contained in:
326
src/main.rs
Normal file
326
src/main.rs
Normal file
@@ -0,0 +1,326 @@
|
||||
#![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
|
||||
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>) {
|
||||
// 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();
|
||||
|
||||
let result = mailer.send(email.into());
|
||||
assert!(result.is_ok());
|
||||
|
||||
mailer.close();
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// 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);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
send_email(&x.name,
|
||||
&x.email,
|
||||
&language_strings.email.subject,
|
||||
&strfmt(&language_strings.email.message, &vars).unwrap(),
|
||||
Some(&language_strings.email.attachment),
|
||||
&settings);
|
||||
}
|
||||
else {
|
||||
return Redirect::to("404");
|
||||
}
|
||||
|
||||
// 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_subject.push_str(&x.name);
|
||||
rsvp_subject.push_str(" just responded to your event!");
|
||||
|
||||
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);
|
||||
|
||||
send_email(&smtp_name, &smtp_username, &rsvp_subject, &rsvp_message, None, &settings);
|
||||
Redirect::to(format!("/thanks/{}", urlencoding::encode(&x.name)))
|
||||
}
|
||||
None => Redirect::to("404")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[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("/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![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();
|
||||
}
|
72
src/settings.rs
Normal file
72
src/settings.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use config::{ConfigError, Config};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct EmailCredentials {
|
||||
pub smtp_server: String,
|
||||
pub smtp_name: String,
|
||||
pub smtp_username: String,
|
||||
pub smtp_password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EmailStrings {
|
||||
pub subject: String,
|
||||
pub message: String,
|
||||
pub attachment: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FormStrings {
|
||||
pub title: String,
|
||||
pub h1: String,
|
||||
pub h2: String,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub num_guests: String,
|
||||
pub cant_come: String,
|
||||
pub one_guest: String,
|
||||
pub two_guest: String,
|
||||
pub three_guest: String,
|
||||
pub four_guest: String,
|
||||
pub five_guest: String,
|
||||
pub submit: String,
|
||||
pub additional_info: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageStrings {
|
||||
pub title: String,
|
||||
pub h1: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LanguageStrings {
|
||||
pub email: EmailStrings,
|
||||
pub form: FormStrings,
|
||||
pub thanks: MessageStrings,
|
||||
pub spam:MessageStrings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub email_credentials: EmailCredentials,
|
||||
pub spam_wait: u64,
|
||||
pub tld_mapping: HashMap<String, String>,
|
||||
pub default_lang: String,
|
||||
pub languages: HashMap<String, LanguageStrings>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn new() -> Result<Self,ConfigError> {
|
||||
let mut settings = Config::default();
|
||||
settings
|
||||
// File::with_name(..) is shorthand for File::from(Path::new(..))
|
||||
.merge(config::File::with_name("config.hjson")).unwrap();
|
||||
|
||||
|
||||
settings.try_into()
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user