Initial mail support.
Deletion and verification mails are now sent via sendmail
This commit is contained in:
parent
49f15fd5e0
commit
a7937158ff
|
@ -20,6 +20,13 @@ parking_lot = "0.6"
|
||||||
structopt = "0.2"
|
structopt = "0.2"
|
||||||
url = "1.6"
|
url = "1.6"
|
||||||
hex = "0.3"
|
hex = "0.3"
|
||||||
|
lettre_email = "0.8"
|
||||||
|
|
||||||
|
[dependencies.lettre]
|
||||||
|
version = "0.8"
|
||||||
|
default-features = false
|
||||||
|
# smtp-transport doesn't build (openssl problem)
|
||||||
|
features = ["file-transport", "sendmail-transport"]
|
||||||
|
|
||||||
[dependencies.rocket_contrib]
|
[dependencies.rocket_contrib]
|
||||||
version = "0"
|
version = "0"
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
use rocket_contrib::Template;
|
||||||
|
|
||||||
|
use lettre::{SendmailTransport, EmailTransport};
|
||||||
|
use lettre_email::EmailBuilder;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use Result;
|
||||||
|
use types::Email;
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct Context{
|
||||||
|
pub token: String,
|
||||||
|
pub userid: String,
|
||||||
|
pub domain: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_mail<T>(to: &Email, subject: &str, template_dir: &str,
|
||||||
|
template_base: &str, domain: &str, ctx: T)
|
||||||
|
-> Result<()> where T: Serialize + Clone
|
||||||
|
{
|
||||||
|
let html = Template::show(template_dir, format!("{}-html", template_base), ctx.clone());
|
||||||
|
let txt = Template::show(template_dir, format!("{}-txt", template_base), ctx);
|
||||||
|
let email = EmailBuilder::new()
|
||||||
|
.to(to.to_string())
|
||||||
|
.from(format!("noreply@{}", domain))
|
||||||
|
.subject(subject)
|
||||||
|
.alternative(
|
||||||
|
html.ok_or("Email template failed to render")?,
|
||||||
|
txt.ok_or("Email template failed to render")?)
|
||||||
|
.build().unwrap();
|
||||||
|
|
||||||
|
let mut sender = SendmailTransport::new();
|
||||||
|
|
||||||
|
sender.send(&email)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_verification_mail(userid: &Email, token: &str, template_dir: &str,
|
||||||
|
domain: &str)
|
||||||
|
-> Result<()>
|
||||||
|
{
|
||||||
|
let ctx = Context{
|
||||||
|
token: token.to_string(),
|
||||||
|
userid: userid.to_string(),
|
||||||
|
domain: domain.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
send_mail(userid, "Please verify your email address", template_dir,
|
||||||
|
"verify-email", domain, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_confirmation_mail(userid: &Email, token: &str, template_dir: &str,
|
||||||
|
domain: &str)
|
||||||
|
-> Result<()>
|
||||||
|
{
|
||||||
|
let ctx = Context{
|
||||||
|
token: token.to_string(),
|
||||||
|
userid: userid.to_string(),
|
||||||
|
domain: domain.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
send_mail(userid, "Please confirm deletion of your key", template_dir,
|
||||||
|
"confirm-email", domain, ctx)
|
||||||
|
}
|
|
@ -23,10 +23,13 @@ extern crate rand;
|
||||||
extern crate tempfile;
|
extern crate tempfile;
|
||||||
extern crate parking_lot;
|
extern crate parking_lot;
|
||||||
extern crate structopt;
|
extern crate structopt;
|
||||||
|
extern crate lettre;
|
||||||
|
extern crate lettre_email;
|
||||||
|
|
||||||
mod web;
|
mod web;
|
||||||
mod database;
|
mod database;
|
||||||
mod types;
|
mod types;
|
||||||
|
mod mail;
|
||||||
|
|
||||||
mod errors {
|
mod errors {
|
||||||
error_chain!{
|
error_chain!{
|
||||||
|
@ -39,6 +42,7 @@ mod errors {
|
||||||
StringUtf8Error(::std::string::FromUtf8Error);
|
StringUtf8Error(::std::string::FromUtf8Error);
|
||||||
StrUtf8Error(::std::str::Utf8Error);
|
StrUtf8Error(::std::str::Utf8Error);
|
||||||
HexError(::hex::FromHexError);
|
HexError(::hex::FromHexError);
|
||||||
|
SendmailError(::lettre::sendmail::error::Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +66,11 @@ pub struct Opt {
|
||||||
/// Port and address to listen on.
|
/// Port and address to listen on.
|
||||||
#[structopt(short = "l", long = "listen", default_value = "0.0.0.0:8080")]
|
#[structopt(short = "l", long = "listen", default_value = "0.0.0.0:8080")]
|
||||||
listen: String,
|
listen: String,
|
||||||
|
/// FQDN of the server. Used in templates.
|
||||||
|
#[structopt(short = "D", long = "domain", default_value = "localhost")]
|
||||||
|
domain: String,
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -50,6 +50,8 @@ mod templates {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StaticDir(String);
|
struct StaticDir(String);
|
||||||
|
pub struct MailTemplateDir(String);
|
||||||
|
pub struct Domain(String);
|
||||||
|
|
||||||
impl<'a, 'r> FromRequest<'a, 'r> for queries::Hkp {
|
impl<'a, 'r> FromRequest<'a, 'r> for queries::Hkp {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
@ -165,10 +167,13 @@ fn verify(db: rocket::State<Polymorphic>, token: String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/delete/<fpr>")]
|
#[get("/vks/delete/<fpr>")]
|
||||||
fn delete(db: rocket::State<Polymorphic>, fpr: String)
|
fn delete(db: rocket::State<Polymorphic>, fpr: String,
|
||||||
|
tmpl: State<MailTemplateDir>, domain: State<Domain>)
|
||||||
-> result::Result<Template, Custom<String>>
|
-> result::Result<Template, Custom<String>>
|
||||||
{
|
{
|
||||||
|
use mail::send_confirmation_mail;
|
||||||
|
|
||||||
let fpr = match Fingerprint::from_str(&fpr) {
|
let fpr = match Fingerprint::from_str(&fpr) {
|
||||||
Ok(fpr) => fpr,
|
Ok(fpr) => fpr,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
@ -286,6 +291,7 @@ pub fn serve(opt: &Opt, db: Polymorphic) -> Result<()> {
|
||||||
.root(opt.base.clone())
|
.root(opt.base.clone())
|
||||||
.extra("template_dir", format!("{}/templates", opt.base.display()))
|
.extra("template_dir", format!("{}/templates", opt.base.display()))
|
||||||
.extra("static_dir", format!("{}/public", opt.base.display()))
|
.extra("static_dir", format!("{}/public", opt.base.display()))
|
||||||
|
.extra("domain", opt.domain.clone())
|
||||||
.finalize()?;
|
.finalize()?;
|
||||||
let routes = routes![
|
let routes = routes![
|
||||||
// infra
|
// infra
|
||||||
|
@ -313,6 +319,23 @@ pub fn serve(opt: &Opt, db: Polymorphic) -> Result<()> {
|
||||||
|
|
||||||
Ok(rocket.manage(StaticDir(static_dir)))
|
Ok(rocket.manage(StaticDir(static_dir)))
|
||||||
}))
|
}))
|
||||||
|
.attach(AdHoc::on_attach(|rocket| {
|
||||||
|
let static_dir = rocket.config()
|
||||||
|
.get_str("template_dir")
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(rocket.manage(MailTemplateDir(static_dir)))
|
||||||
|
}))
|
||||||
|
.attach(AdHoc::on_attach(|rocket| {
|
||||||
|
let static_dir = rocket.config()
|
||||||
|
.get_str("domain")
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(rocket.manage(Domain(static_dir)))
|
||||||
|
}))
|
||||||
|
|
||||||
.mount("/", routes)
|
.mount("/", routes)
|
||||||
.manage(db)
|
.manage(db)
|
||||||
.launch();
|
.launch();
|
||||||
|
|
|
@ -7,8 +7,14 @@ use rocket::http::{ContentType, Status};
|
||||||
use rocket::response::status::Custom;
|
use rocket::response::status::Custom;
|
||||||
use rocket_contrib::Template;
|
use rocket_contrib::Template;
|
||||||
|
|
||||||
|
use types::Email;
|
||||||
|
use mail::send_verification_mail;
|
||||||
|
use web::{Domain, MailTemplateDir};
|
||||||
use database::{Database, Polymorphic};
|
use database::{Database, Polymorphic};
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
mod template {
|
mod template {
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
|
@ -17,82 +23,132 @@ mod template {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Context {
|
pub struct Verify {
|
||||||
pub tokens: Vec<Token>,
|
pub tokens: Vec<Token>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/keys", data = "<data>")]
|
#[post("/pks/add", data = "<data>")]
|
||||||
// signature requires the request to have a `Content-Type`
|
// signature requires the request to have a `Content-Type`
|
||||||
pub fn multipart_upload(db: State<Polymorphic>, cont_type: &ContentType, data: Data) -> Result<Template, Custom<String>> {
|
pub fn multipart_upload(db: State<Polymorphic>, cont_type: &ContentType,
|
||||||
// this and the next check can be implemented as a request guard but it seems like just
|
data: Data, tmpl: State<MailTemplateDir>,
|
||||||
// more boilerplate than necessary
|
domain: State<Domain>)
|
||||||
if !cont_type.is_form_data() {
|
-> Result<Template, Custom<String>>
|
||||||
return Err(Custom(
|
{
|
||||||
Status::BadRequest,
|
if cont_type.is_form_data() {
|
||||||
"Content-Type not multipart/form-data".into()
|
// multipart/form-data
|
||||||
));
|
let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else(
|
||||||
}
|
|
||||||
|
|
||||||
let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else(
|
|
||||||
|| Custom(
|
|| Custom(
|
||||||
Status::BadRequest,
|
Status::BadRequest,
|
||||||
"`Content-Type: multipart/form-data` boundary param not provided".into()
|
"`Content-Type: multipart/form-data` boundary param not provided".into()
|
||||||
)
|
)
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
process_upload(boundary, data, db.inner())
|
process_upload(boundary, data, db.inner(), &tmpl.0, &domain.0)
|
||||||
|
} else if cont_type.is_form() {
|
||||||
|
use rocket::request::FormItems;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
// application/x-www-form-urlencoded
|
||||||
|
let mut buf = Vec::default();
|
||||||
|
|
||||||
|
data.stream_to(&mut buf).or_else(|_| {
|
||||||
|
Err(Custom(Status::BadRequest,
|
||||||
|
"`Content-Type: application/x-www-form-urlencoded` not valid".into()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for (key, value) in FormItems::from(&*String::from_utf8_lossy(&buf)) {
|
||||||
|
let decoded_value = value.url_decode().or_else(|_| {
|
||||||
|
Err(Custom(Status::BadRequest,
|
||||||
|
"`Content-Type: application/x-www-form-urlencoded` not valid".into()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match key.as_str() {
|
||||||
|
"keytext" => {
|
||||||
|
return process_key(Cursor::new(decoded_value.as_bytes()),
|
||||||
|
&db, &tmpl.0, &domain.0);
|
||||||
|
}
|
||||||
|
_ => { /* skip */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Custom(Status::BadRequest, "Not a PGP public key".into()))
|
||||||
|
} else {
|
||||||
|
Err(Custom(Status::BadRequest, "Content-Type not a form".into()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_upload(boundary: &str, data: Data, db: &Polymorphic) -> Result<Template, Custom<String>> {
|
fn process_upload(boundary: &str, data: Data, db: &Polymorphic, tmpl: &str,
|
||||||
|
domain: &str)
|
||||||
|
-> Result<Template, Custom<String>>
|
||||||
|
{
|
||||||
// saves all fields, any field longer than 10kB goes to a temporary directory
|
// saves all fields, any field longer than 10kB goes to a temporary directory
|
||||||
// Entries could implement FromData though that would give zero control over
|
// Entries could implement FromData though that would give zero control over
|
||||||
// how the files are saved; Multipart would be a good impl candidate though
|
// how the files are saved; Multipart would be a good impl candidate though
|
||||||
match Multipart::with_body(data.open(), boundary).save().temp() {
|
match Multipart::with_body(data.open(), boundary).save().temp() {
|
||||||
Full(entries) => process_entries(entries, db),
|
Full(entries) => process_multipart(entries, db, tmpl, domain),
|
||||||
Partial(partial, _) => {
|
Partial(partial, _) => process_multipart(partial.entries, db, tmpl, domain),
|
||||||
process_entries(partial.entries, db)
|
|
||||||
},
|
|
||||||
Error(err) => Err(Custom(Status::InternalServerError, err.to_string())),
|
Error(err) => Err(Custom(Status::InternalServerError, err.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// having a streaming output would be nice; there's one for returning a `Read` impl
|
fn process_multipart(entries: Entries, db: &Polymorphic, tmpl: &str,
|
||||||
// but not one that you can `write()` to
|
domain: &str)
|
||||||
fn process_entries(entries: Entries, db: &Polymorphic) -> Result<Template, Custom<String>> {
|
-> Result<Template, Custom<String>>
|
||||||
use openpgp::TPK;
|
{
|
||||||
|
match entries.fields.get(&"keytext".to_string()) {
|
||||||
match entries.fields.get(&"key".to_string()) {
|
|
||||||
Some(ent) if ent.len() == 1 => {
|
Some(ent) if ent.len() == 1 => {
|
||||||
let reader = ent[0].data.readable().map_err(|err| {
|
let reader = ent[0].data.readable().map_err(|err| {
|
||||||
Custom(Status::InternalServerError, err.to_string())
|
Custom(Status::InternalServerError, err.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match TPK::from_reader(reader) {
|
process_key(reader, db, tmpl, domain)
|
||||||
Ok(tpk) => {
|
|
||||||
match db.merge_or_publish(tpk) {
|
|
||||||
Ok(tokens) => {
|
|
||||||
let tokens = tokens
|
|
||||||
.into_iter().map(|(uid,tok)| {
|
|
||||||
template::Token{ userid: uid.to_string(), token: tok }
|
|
||||||
}).collect::<Vec<_>>();
|
|
||||||
let context = template::Context{
|
|
||||||
tokens: tokens
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Template::render("upload", context))
|
|
||||||
}
|
|
||||||
Err(err) =>
|
|
||||||
Err(Custom(Status::InternalServerError,
|
|
||||||
format!("{:?}", err))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => Err(Custom(Status::BadRequest,
|
|
||||||
"Not a PGP public key".into())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(_) | None =>
|
Some(_) | None =>
|
||||||
Err(Custom(Status::BadRequest, "Not a PGP public key".into())),
|
Err(Custom(Status::BadRequest, "Not a PGP public key".into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process_key<R>(reader: R, db: &Polymorphic, tmpl: &str, domain: &str)
|
||||||
|
-> Result<Template, Custom<String>> where R: Read
|
||||||
|
{
|
||||||
|
use openpgp::{Reader, TPK};
|
||||||
|
let reader = Reader::from_reader(reader).or_else(|_| {
|
||||||
|
Err(Custom(Status::BadRequest,
|
||||||
|
"`Content-Type: application/x-www-form-urlencoded` not valid".into()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match TPK::from_reader(reader) {
|
||||||
|
Ok(tpk) => {
|
||||||
|
match db.merge_or_publish(tpk) {
|
||||||
|
Ok(tokens) => {
|
||||||
|
let tokens = tokens
|
||||||
|
.into_iter().map(|(uid,tok)| {
|
||||||
|
template::Token{ userid: uid.to_string(), token: tok }
|
||||||
|
}).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// send out emails
|
||||||
|
for tok in tokens.iter() {
|
||||||
|
let &template::Token{ ref userid, ref token } = tok;
|
||||||
|
|
||||||
|
Email::from_str(userid).and_then(|email| {
|
||||||
|
send_verification_mail(&email, token, tmpl, domain)
|
||||||
|
}).map_err(|err| {
|
||||||
|
Custom(Status::InternalServerError, format!("{:?}", err))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = template::Verify{
|
||||||
|
tokens: tokens
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Template::render("upload", context))
|
||||||
|
}
|
||||||
|
Err(err) =>
|
||||||
|
Err(Custom(Status::InternalServerError,
|
||||||
|
format!("{:?}", err))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err(Custom(Status::BadRequest, "Not a PGP public key".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>Upload your key</h2>
|
<h2>Upload your key</h2>
|
||||||
<form action="/keys" method=POST enctype=multipart/form-data>
|
<form action="/pks/add" method=POST enctype=multipart/form-data>
|
||||||
<input type="file" id="key" name="key" placeholder="Your public key">
|
<input type="file" id="keytext" name="keytext" placeholder="Your public key">
|
||||||
<input type="submit" value="Upload">
|
<input type="submit" value="Upload">
|
||||||
</form>
|
</form>
|
||||||
{{/layout}}
|
{{/layout}}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{{#if tokens}}
|
{{#if tokens}}
|
||||||
<ul>
|
<ul>
|
||||||
{{#each tokens}}
|
{{#each tokens}}
|
||||||
<li>{{userid}}: <a href="/verify/{{token}}"><tt>{{token}}</tt></a></li>
|
<li>{{userid}}: <a href="/vks/verify/{{token}}"><tt>{{token}}</tt></a></li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{{#if verified }}
|
{{#if verified }}
|
||||||
<h1>Email verified</h1>
|
<h1>Email verified</h1>
|
||||||
<p>You've verified <em>{{ userid }}</em> successfully. Everybody who knows
|
<p>You've verified <em>{{ userid }}</em> successfully. Everybody who knows
|
||||||
your email address is no able to find your key. You can <a href="/delete/{{
|
your email address is no able to find your key. You can <a href="/vks/delete/{{
|
||||||
fpr }}">delete</a> your key any time you want.</p>
|
fpr }}">delete</a> your key any time you want.</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<h1>Email verification failed</h1>
|
<h1>Email verification failed</h1>
|
||||||
|
|
Loading…
Reference in New Issue