Initial implementation
This commit is contained in:
commit
241ecb1099
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/templates/
|
2587
Cargo.lock
generated
Normal file
2587
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal 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
8
shell.nix
Normal file
|
@ -0,0 +1,8 @@
|
|||
with import <nixpkgs> {};
|
||||
|
||||
runCommand "www" {
|
||||
buildInputs = [
|
||||
openssl
|
||||
pkg-config
|
||||
];
|
||||
} ""
|
38
src/context.rs
Normal file
38
src/context.rs
Normal 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
50
src/mail.rs
Normal 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
20
src/main.rs
Normal 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
78
src/static_templates.rs
Normal 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]
|
||||
}
|
Loading…
Reference in a new issue