Initial implementation

This commit is contained in:
Tristan Daniël Maat 2020-05-31 00:45:47 +01:00
commit 241ecb1099
Signed by: tlater
GPG key ID: 49670FD774E43268
9 changed files with 2797 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/templates/

2587
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "tlaternet"
version = "0.1.0"
authors = ["Tristan Daniël Maat <tm@tlater.net>"]
edition = "2018"
[dependencies]
check-if-email-exists = "0.8.5"
lettre = "0.9.3"
lettre_email = "0.9.3"
rocket = "0.4.4"
rocket_contrib = { version = "0.4.4", features = [ "handlebars_templates", "serve" ] }
serde = { version = "1.0.111", features = [ "derive" ] }

8
shell.nix Normal file
View file

@ -0,0 +1,8 @@
with import <nixpkgs> {};
runCommand "www" {
buildInputs = [
openssl
pkg-config
];
} ""

38
src/context.rs Normal file
View file

@ -0,0 +1,38 @@
use rocket::request::FlashMessage;
use serde::Serialize;
#[derive(Default, Serialize)]
pub(crate) struct Context {
pub flash: Option<Flash>,
}
#[allow(dead_code)] // Want to keep all possible flash types
#[derive(Serialize)]
#[serde(rename_all = "lowercase", tag = "type", content = "message")]
pub(crate) enum Flash {
Primary(String),
Secondary(String),
Success(String),
Danger(String),
Warning(String),
Info(String),
Light(String),
Dark(String),
}
impl From<FlashMessage<'_, '_>> for Flash {
fn from(message: FlashMessage) -> Self {
match message.name() {
"success" => Self::Success(message.msg().to_string()),
"warning" => Self::Warning(message.msg().to_string()),
"error" => Self::Danger(message.msg().to_string()),
"primary" => Self::Primary(message.msg().to_string()),
"secondary" => Self::Secondary(message.msg().to_string()),
"info" => Self::Info(message.msg().to_string()),
"light" => Self::Light(message.msg().to_string()),
"dark" => Self::Dark(message.msg().to_string()),
name => Self::Info(format!("{}: {}", name, message.msg())),
}
}
}

50
src/mail.rs Normal file
View file

@ -0,0 +1,50 @@
use std::fs::create_dir_all;
use lettre::{FileTransport, Transport};
use lettre_email::EmailBuilder;
use rocket::request::{Form, FromForm};
use rocket::response::{Flash, Redirect};
use rocket::{post, routes, Route};
#[derive(FromForm)]
struct Email {
mail: String,
subject: String,
message: String,
}
fn send_mail(email: &Email) -> Result<(), String> {
let email = EmailBuilder::new()
.to("tm@tlater.net")
.from(email.mail.clone())
.subject(email.subject.clone())
.text(email.message.clone())
.build()
.map_err(|err| format!("Invalid email contents: {}", err))?;
let mut mailer = FileTransport::new("mails");
if let Err(error) = create_dir_all("mails") {
println!("Could not create mail directory: {}", error);
};
mailer.send(email.into()).map_err(|err| {
println!("Could not save mail: {}", err);
"Failed to send email due to internal issues; please try again later".to_string()
})?;
Ok(())
}
#[post("/mail.html", data = "<email>")]
fn mail_post(email: Form<Email>) -> Result<Flash<Redirect>, Flash<Redirect>> {
match send_mail(&email) {
Ok(_) => Ok(Flash::success(
Redirect::to("/mail.html"),
"Email sent successfully",
)),
Err(err) => Err(Flash::error(Redirect::to("/mail.html"), err)),
}
}
pub fn routes() -> Vec<Route> {
routes![mail_post]
}

20
src/main.rs Normal file
View file

@ -0,0 +1,20 @@
#![feature(proc_macro_hygiene, decl_macro)]
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template;
mod context;
mod mail;
mod static_templates;
use mail::routes as mail_routes;
use static_templates::routes as static_templates;
fn main() {
rocket::ignite()
.attach(Template::fairing())
.mount("/", mail_routes())
.mount("/", static_templates())
.mount("/", StaticFiles::from("templates"))
.launch();
}

78
src/static_templates.rs Normal file
View file

@ -0,0 +1,78 @@
/// Serve templates "statically".
///
/// In the generic case, we don't want to do any processing on
/// template pages. They should be served "statically", but we want to
/// transform the bits of default handlebars templating - currently
/// setting the flash to nothing.
///
/// This module implements a catchall route for this purpose.
use rocket::request::FlashMessage;
use std::ffi::OsString;
use rocket::http::uri::Segments;
use rocket::request::FromSegments;
use rocket::response::status;
use rocket::{get, routes, Route};
use rocket_contrib::templates::Template;
use crate::context::Context;
pub struct HTMLPage {
pub page: OsString,
}
impl<'a> FromSegments<'a> for HTMLPage {
type Error = &'static str;
fn from_segments(segments: Segments<'a>) -> Result<Self, Self::Error> {
let page = segments
.into_path_buf(false)
.map_err(|_| "Invalid segments")?;
page.extension()
.map(|extension| {
if extension == "html" {
Some(HTMLPage {
page: page
.file_stem()
.expect("Should exist if the extension does")
.to_os_string(),
})
} else {
None
}
})
.flatten()
.ok_or("Invalid page name")
}
}
#[get("/")]
pub fn static_index() -> Template {
Template::render("index", Context::default())
}
#[get("/<path..>")]
pub fn static_templates(
path: HTMLPage,
flash: Option<FlashMessage>,
) -> Result<Template, status::BadRequest<String>> {
let path = path.page.into_string().map_err(|path| {
status::BadRequest(Some(format!("Invalid path: {}", path.to_string_lossy())))
})?;
if let Some(flash) = flash {
Ok(Template::render(
path,
Context {
flash: Some(flash.into()),
},
))
} else {
Ok(Template::render(path, Context::default()))
}
}
pub fn routes() -> Vec<Route> {
routes![static_index, static_templates]
}