Introduce mail::Service.

- This conveniently combines From and MailTemplates, and will allow
    use to introduce new transports.
This commit is contained in:
Justus Winter 2019-03-04 16:43:10 +01:00
parent e735758e50
commit 4459dc9bac
No known key found for this signature in database
GPG Key ID: 686F55B4AB2B3386
3 changed files with 112 additions and 106 deletions

View File

@ -14,78 +14,93 @@ pub struct Context {
pub domain: String,
}
fn send_mail<T>(
to: &Email, subject: &str, mail_templates: &Handlebars, template: &str,
from: &str, ctx: T,
) -> Result<()>
where
T: Serialize + Clone,
{
let tmpl_html = format!("{}-html", template);
let tmpl_txt = format!("{}-txt", template);
let (html, txt) = {
if let (Ok(inner_html), Ok(inner_txt)) = (
mail_templates.render(&tmpl_html, &ctx),
mail_templates.render(&tmpl_txt, &ctx),
) {
(Some(inner_html), Some(inner_txt))
} else {
(None, None)
pub struct Service {
from: String,
templates: Handlebars,
transport: Transport,
}
enum Transport {
Sendmail,
}
impl Service {
/// Sends mail via sendmail.
pub fn sendmail(from: String, templates: Handlebars) -> Self {
Self {
from: from,
templates: templates,
transport: Transport::Sendmail,
}
};
}
let email = EmailBuilder::new()
.to(to.to_string())
.from(from)
.subject(subject)
.alternative(
html.ok_or(failure::err_msg("Email template failed to render"))?,
txt.ok_or(failure::err_msg("Email template failed to render"))?,
pub fn send_verification(&self, userid: &Email, token: &str, domain: &str)
-> Result<()> {
let ctx = Context {
token: token.to_string(),
userid: userid.to_string(),
domain: domain.to_string(),
};
self.send(
userid,
"Please verify your email address",
"verify",
ctx,
)
.build()
.unwrap();
}
let mut sender = SendmailTransport::new();
sender.send(&email)?;
Ok(())
}
pub fn send_verification_mail(
userid: &Email, token: &str, mail_templates: &Handlebars, domain: &str,
from: &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",
mail_templates,
"verify",
from,
ctx,
)
}
pub fn send_confirmation_mail(
userid: &Email, token: &str, mail_templates: &Handlebars, domain: &str,
from: &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",
mail_templates,
"confirm",
from,
ctx,
)
pub fn send_confirmation(&self, userid: &Email, token: &str, domain: &str)
-> Result<()> {
let ctx = Context {
token: token.to_string(),
userid: userid.to_string(),
domain: domain.to_string(),
};
self.send(
userid,
"Please confirm deletion of your key",
"confirm",
ctx,
)
}
fn send<T>(&self, to: &Email, subject: &str, template: &str, ctx: T)
-> Result<()>
where T: Serialize + Clone,
{
let tmpl_html = format!("{}-html", template);
let tmpl_txt = format!("{}-txt", template);
let (html, txt) = {
if let (Ok(inner_html), Ok(inner_txt)) = (
self.templates.render(&tmpl_html, &ctx),
self.templates.render(&tmpl_txt, &ctx),
) {
(Some(inner_html), Some(inner_txt))
} else {
(None, None)
}
};
let email = EmailBuilder::new()
.to(to.to_string())
.from(self.from.clone())
.subject(subject)
.alternative(
html.ok_or(failure::err_msg("Email template failed to render"))?,
txt.ok_or(failure::err_msg("Email template failed to render"))?,
)
.build()
.unwrap();
match self.transport {
Transport::Sendmail => {
let mut transport = SendmailTransport::new();
transport.send(&email)?;
},
}
Ok(())
}
}

View File

@ -16,6 +16,7 @@ use handlebars::Handlebars;
use std::path::{Path, PathBuf};
mod upload;
use mail;
use database::{Database, Polymorphic, Query};
use Result;
@ -190,8 +191,6 @@ mod templates {
struct StaticDir(String);
pub struct Domain(String);
pub struct From(String);
pub struct MailTemplates(Handlebars);
pub struct XAccelRedirect(bool);
impl<'a, 'r> FromRequest<'a, 'r> for queries::Hkp {
@ -470,11 +469,10 @@ struct ManageRequest {
#[post("/vks/v1/manage", data="<request>")]
fn manage_post(
db: State<Polymorphic>, tmpl: State<MailTemplates>, domain: State<Domain>,
from: State<From>, request: Form<ManageRequest>,
db: State<Polymorphic>, mail_service: State<mail::Service>,
domain: State<Domain>, request: Form<ManageRequest>,
) -> MyResponse {
use std::convert::TryInto;
use mail::send_confirmation_mail;
let query = match request.search_term.parse() {
Ok(query) => query,
@ -498,8 +496,8 @@ fn manage_post(
};
for uid in uids {
if let Err(e) = send_confirmation_mail(
&uid, &token, &tmpl.0, &domain.0, &from.0) {
if let Err(e) = mail_service.send_confirmation(
&uid, &token, &domain.0) {
return MyResponse::ise(e);
}
}
@ -683,24 +681,21 @@ fn rocket_factory(rocket: rocket::Rocket, db: Polymorphic) -> rocket::Rocket {
Ok(rocket.manage(Domain(domain)))
}))
.attach(AdHoc::on_attach("from", |rocket| {
let from = rocket.config().get_str("from").unwrap().to_string();
Ok(rocket.manage(From(from)))
}))
.attach(AdHoc::on_attach("x-accel-redirect", |rocket| {
let x_accel_redirect =
rocket.config().get_bool("x-accel-redirect").unwrap();
Ok(rocket.manage(XAccelRedirect(x_accel_redirect)))
}))
.attach(AdHoc::on_attach("mail_templates", |rocket| {
.attach(AdHoc::on_attach("mail-service", |rocket| {
let dir: PathBuf = rocket
.config()
.get_str("template_dir")
.unwrap()
.to_string()
.into();
let from = rocket.config().get_str("from").unwrap().to_string();
let confirm_html = dir.join("confirm-email-html.hbs");
let confirm_txt = dir.join("confirm-email-txt.hbs");
let verify_html = dir.join("verify-email-html.hbs");
@ -720,7 +715,7 @@ fn rocket_factory(rocket: rocket::Rocket, db: Polymorphic) -> rocket::Rocket {
.register_template_file("verify-txt", verify_txt)
.unwrap();
Ok(rocket.manage(MailTemplates(handlebars)))
Ok(rocket.manage(mail::Service::sendmail(from, handlebars)))
}))
.mount("/", routes)
.manage(db)

View File

@ -9,11 +9,9 @@ use rocket::{Data, State};
use rocket_contrib::templates::Template;
use rocket::response::Flash;
use handlebars::Handlebars;
use database::{Database, Polymorphic};
use mail::send_verification_mail;
use web::{Domain, From, MailTemplates};
use mail;
use web::Domain;
use std::io::Read;
@ -80,9 +78,9 @@ fn show_error(error: String) -> Template {
#[post("/vks/v1/publish/submit", data = "<data>")]
pub fn vks_publish_submit(
db: State<Polymorphic>, cont_type: &ContentType, data: Data,
tmpl: State<MailTemplates>, domain: State<Domain>, from: State<From>
mail_service: State<mail::Service>, domain: State<Domain>,
) -> Flash<Redirect> {
match do_upload_hkp(db, cont_type, data, tmpl, domain, from) {
match do_upload_hkp(db, cont_type, data, mail_service, domain) {
Ok(ok) => ok,
Err(err) => Flash::error(Redirect::to("/vks/v1/publish?err"), err.to_string()),
}
@ -91,14 +89,14 @@ pub fn vks_publish_submit(
// signature requires the request to have a `Content-Type`
fn do_upload_hkp(
db: State<Polymorphic>, cont_type: &ContentType, data: Data,
tmpl: State<MailTemplates>, domain: State<Domain>, from: State<From>,
mail_service: State<mail::Service>, domain: State<Domain>,
) -> Result<Flash<Redirect>, String> {
if cont_type.is_form_data() {
// multipart/form-data
let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else(
|| "`Content-Type: multipart/form-data` boundary param not provided".to_owned())?;
process_upload(boundary, data, db.inner(), &tmpl.0, &domain.0, &from.0)
process_upload(boundary, data, db.inner(), mail_service, &domain.0)
} else if cont_type.is_form() {
use rocket::request::FormItems;
use std::io::Cursor;
@ -121,9 +119,8 @@ fn do_upload_hkp(
return process_key(
Cursor::new(decoded_value.as_bytes()),
&db,
&tmpl.0,
mail_service,
&domain.0,
&from.0,
);
}
_ => { /* skip */ }
@ -137,26 +134,27 @@ fn do_upload_hkp(
}
fn process_upload(
boundary: &str, data: Data, db: &Polymorphic, mail_templates: &Handlebars,
domain: &str, from: &str,
boundary: &str, data: Data, db: &Polymorphic,
mail_service: State<mail::Service>,
domain: &str,
) -> Result<Flash<Redirect>, 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_multipart(entries, db, mail_templates, domain, from)
process_multipart(entries, db, mail_service, domain)
}
Partial(partial, _) => {
process_multipart(partial.entries, db, mail_templates, domain, from)
process_multipart(partial.entries, db, mail_service, domain)
}
Error(err) => Err(err.to_string())
}
}
fn process_multipart(
entries: Entries, db: &Polymorphic, mail_templates: &Handlebars,
domain: &str, from: &str,
entries: Entries, db: &Polymorphic, mail_service: State<mail::Service>,
domain: &str,
) -> Result<Flash<Redirect>, String> {
match entries.fields.get("keytext") {
Some(ent) if ent.len() == 1 => {
@ -164,7 +162,7 @@ fn process_multipart(
err.to_string()
})?;
process_key(reader, db, mail_templates, domain, from)
process_key(reader, db, mail_service, domain)
}
Some(_) | None => {
Err("Not a PGP public key".into())
@ -173,8 +171,8 @@ fn process_multipart(
}
fn process_key<R>(
reader: R, db: &Polymorphic, mail_templates: &Handlebars, domain: &str,
from: &str
reader: R, db: &Polymorphic, mail_service: State<mail::Service>,
domain: &str,
) -> Result<Flash<Redirect>, String>
where
R: Read,
@ -188,12 +186,10 @@ where
let mut results: Vec<String> = vec!();
for (email,token) in tokens {
send_verification_mail(
mail_service.send_verification(
&email,
&token,
mail_templates,
domain,
from,
).map_err(|e| format!("{}", e))?;
results.push(email.to_string());
}