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"
|
||||
url = "1.6"
|
||||
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]
|
||||
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 parking_lot;
|
||||
extern crate structopt;
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
|
||||
mod web;
|
||||
mod database;
|
||||
mod types;
|
||||
mod mail;
|
||||
|
||||
mod errors {
|
||||
error_chain!{
|
||||
|
@ -39,6 +42,7 @@ mod errors {
|
|||
StringUtf8Error(::std::string::FromUtf8Error);
|
||||
StrUtf8Error(::std::str::Utf8Error);
|
||||
HexError(::hex::FromHexError);
|
||||
SendmailError(::lettre::sendmail::error::Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +66,11 @@ pub struct Opt {
|
|||
/// Port and address to listen on.
|
||||
#[structopt(short = "l", long = "listen", default_value = "0.0.0.0:8080")]
|
||||
listen: String,
|
||||
/// FQDN of the server. Used in templates.
|
||||
#[structopt(short = "D", long = "domain", default_value = "localhost")]
|
||||
domain: String,
|
||||
|
||||
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
|
|
@ -50,6 +50,8 @@ mod templates {
|
|||
}
|
||||
|
||||
struct StaticDir(String);
|
||||
pub struct MailTemplateDir(String);
|
||||
pub struct Domain(String);
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for queries::Hkp {
|
||||
type Error = ();
|
||||
|
@ -165,10 +167,13 @@ fn verify(db: rocket::State<Polymorphic>, token: String)
|
|||
}
|
||||
}
|
||||
|
||||
#[get("/delete/<fpr>")]
|
||||
fn delete(db: rocket::State<Polymorphic>, fpr: String)
|
||||
#[get("/vks/delete/<fpr>")]
|
||||
fn delete(db: rocket::State<Polymorphic>, fpr: String,
|
||||
tmpl: State<MailTemplateDir>, domain: State<Domain>)
|
||||
-> result::Result<Template, Custom<String>>
|
||||
{
|
||||
use mail::send_confirmation_mail;
|
||||
|
||||
let fpr = match Fingerprint::from_str(&fpr) {
|
||||
Ok(fpr) => fpr,
|
||||
Err(_) => {
|
||||
|
@ -286,6 +291,7 @@ pub fn serve(opt: &Opt, db: Polymorphic) -> Result<()> {
|
|||
.root(opt.base.clone())
|
||||
.extra("template_dir", format!("{}/templates", opt.base.display()))
|
||||
.extra("static_dir", format!("{}/public", opt.base.display()))
|
||||
.extra("domain", opt.domain.clone())
|
||||
.finalize()?;
|
||||
let routes = routes![
|
||||
// infra
|
||||
|
@ -313,6 +319,23 @@ pub fn serve(opt: &Opt, db: Polymorphic) -> Result<()> {
|
|||
|
||||
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)
|
||||
.manage(db)
|
||||
.launch();
|
||||
|
|
|
@ -7,8 +7,14 @@ use rocket::http::{ContentType, Status};
|
|||
use rocket::response::status::Custom;
|
||||
use rocket_contrib::Template;
|
||||
|
||||
use types::Email;
|
||||
use mail::send_verification_mail;
|
||||
use web::{Domain, MailTemplateDir};
|
||||
use database::{Database, Polymorphic};
|
||||
|
||||
use std::io::Read;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod template {
|
||||
#[derive(Serialize)]
|
||||
pub struct Token {
|
||||
|
@ -17,23 +23,20 @@ mod template {
|
|||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Context {
|
||||
pub struct Verify {
|
||||
pub tokens: Vec<Token>,
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/keys", data = "<data>")]
|
||||
#[post("/pks/add", data = "<data>")]
|
||||
// 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>> {
|
||||
// this and the next check can be implemented as a request guard but it seems like just
|
||||
// more boilerplate than necessary
|
||||
if !cont_type.is_form_data() {
|
||||
return Err(Custom(
|
||||
Status::BadRequest,
|
||||
"Content-Type not multipart/form-data".into()
|
||||
));
|
||||
}
|
||||
|
||||
pub fn multipart_upload(db: State<Polymorphic>, cont_type: &ContentType,
|
||||
data: Data, tmpl: State<MailTemplateDir>,
|
||||
domain: State<Domain>)
|
||||
-> Result<Template, Custom<String>>
|
||||
{
|
||||
if cont_type.is_form_data() {
|
||||
// multipart/form-data
|
||||
let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else(
|
||||
|| Custom(
|
||||
Status::BadRequest,
|
||||
|
@ -41,33 +44,80 @@ pub fn multipart_upload(db: State<Polymorphic>, cont_type: &ContentType, data: D
|
|||
)
|
||||
)?;
|
||||
|
||||
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
|
||||
// Entries could implement FromData though that would give zero control over
|
||||
// how the files are saved; Multipart would be a good impl candidate though
|
||||
match Multipart::with_body(data.open(), boundary).save().temp() {
|
||||
Full(entries) => process_entries(entries, db),
|
||||
Partial(partial, _) => {
|
||||
process_entries(partial.entries, db)
|
||||
},
|
||||
Full(entries) => process_multipart(entries, db, tmpl, domain),
|
||||
Partial(partial, _) => process_multipart(partial.entries, db, tmpl, domain),
|
||||
Error(err) => Err(Custom(Status::InternalServerError, err.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
// having a streaming output would be nice; there's one for returning a `Read` impl
|
||||
// but not one that you can `write()` to
|
||||
fn process_entries(entries: Entries, db: &Polymorphic) -> Result<Template, Custom<String>> {
|
||||
use openpgp::TPK;
|
||||
|
||||
match entries.fields.get(&"key".to_string()) {
|
||||
fn process_multipart(entries: Entries, db: &Polymorphic, tmpl: &str,
|
||||
domain: &str)
|
||||
-> Result<Template, Custom<String>>
|
||||
{
|
||||
match entries.fields.get(&"keytext".to_string()) {
|
||||
Some(ent) if ent.len() == 1 => {
|
||||
let reader = ent[0].data.readable().map_err(|err| {
|
||||
Custom(Status::InternalServerError, err.to_string())
|
||||
})?;
|
||||
|
||||
process_key(reader, db, tmpl, domain)
|
||||
}
|
||||
Some(_) | None =>
|
||||
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) {
|
||||
|
@ -76,7 +126,19 @@ fn process_entries(entries: Entries, db: &Polymorphic) -> Result<Template, Custo
|
|||
.into_iter().map(|(uid,tok)| {
|
||||
template::Token{ userid: uid.to_string(), token: tok }
|
||||
}).collect::<Vec<_>>();
|
||||
let context = template::Context{
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
|
@ -87,12 +149,6 @@ fn process_entries(entries: Entries, db: &Polymorphic) -> Result<Template, Custo
|
|||
format!("{:?}", err))),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(Custom(Status::BadRequest,
|
||||
"Not a PGP public key".into())),
|
||||
}
|
||||
}
|
||||
Some(_) | None =>
|
||||
Err(Custom(Status::BadRequest, "Not a PGP public key".into())),
|
||||
Err(_) => Err(Custom(Status::BadRequest, "Not a PGP public key".into())),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
</form>
|
||||
|
||||
<h2>Upload your key</h2>
|
||||
<form action="/keys" method=POST enctype=multipart/form-data>
|
||||
<input type="file" id="key" name="key" placeholder="Your public key">
|
||||
<form action="/pks/add" method=POST enctype=multipart/form-data>
|
||||
<input type="file" id="keytext" name="keytext" placeholder="Your public key">
|
||||
<input type="submit" value="Upload">
|
||||
</form>
|
||||
{{/layout}}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{{#if tokens}}
|
||||
<ul>
|
||||
{{#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}}
|
||||
</ul>
|
||||
{{else}}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{{#if verified }}
|
||||
<h1>Email verified</h1>
|
||||
<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>
|
||||
{{else}}
|
||||
<h1>Email verification failed</h1>
|
||||
|
|
Loading…
Reference in New Issue