From f25cda8e68d2ced5a88e08578d5e0941e5c61f0f Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Sat, 5 Dec 2020 14:16:44 +0000 Subject: [PATCH] wip email templates etc. Signed-off-by: Ian Jackson --- nwtemplates/token-other.tera | 2 +- nwtemplates/token-unix.tera | 2 +- src/error.rs | 12 ++++- src/global.rs | 2 +- src/imports.rs | 6 ++- src/lib.rs | 1 + src/nwtemplates.rs | 16 ++++-- src/spec.rs | 96 +++++++++++++++++++++++++----------- 8 files changed, 98 insertions(+), 39 deletions(-) diff --git a/nwtemplates/token-other.tera b/nwtemplates/token-other.tera index 99b19714..5c3441e7 100644 --- a/nwtemplates/token-other.tera +++ b/nwtemplates/token-other.tera @@ -7,7 +7,7 @@ has invited you to join the game {{ game_name }} You can play, directly, by visiting this link in a suitable browser. - {{ token_url }} +{{ token_lines }} They have set for you the nickname, within the game, of {{ nick }} diff --git a/nwtemplates/token-unix.tera b/nwtemplates/token-unix.tera index dacbb4d8..f764d2dc 100644 --- a/nwtemplates/token-unix.tera +++ b/nwtemplates/token-unix.tera @@ -7,7 +7,7 @@ Hi. I'm inviting you to join the game on this Otter server. You can play, directly, by visiting this link in a suitable browser. - {{ token_url }} +{{ token_lines }} I have set for you the nickname, within the game, of {{ nick }} diff --git a/src/error.rs b/src/error.rs index 47ccdf98..4ae9748c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -56,7 +56,17 @@ pub enum InternalError { } #[derive(Clone,Error,Debug,Serialize,Deserialize)] -pub enum TokenDeliveryError { +#[error("{0}")] +pub struct TokenDeliveryError(String); +impl From for TokenDeliveryError { + fn from(a: anyhow::Error) -> TokenDeliveryError { + TokenDeliveryError( + a.chain() + .map(ToString::to_string) + .collect::>() + .join(": ") + ) + } } impl From for SpecError { diff --git a/src/global.rs b/src/global.rs index 9cacd9d4..6faca941 100644 --- a/src/global.rs +++ b/src/global.rs @@ -756,7 +756,7 @@ impl<'ig> InstanceGuard<'ig> { &config().public_url.trim_end_matches("/"), token.0); let info = AccessTokenInfo { url }; - let report = access.deliver(&gpl, &ipl, info)?; + let report = access.deliver(&self.c.g, &gpl, &ipl, info)?; report } diff --git a/src/imports.rs b/src/imports.rs index 783d5738..8206ee6a 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -16,9 +16,10 @@ pub use std::fmt::{self, Display, Debug}; pub use std::fs::File; pub use std::fs; pub use std::hash::Hash; +pub use std::io; pub use std::io::ErrorKind; pub use std::io::{BufReader, Read, BufRead, BufWriter, Write}; -pub use std::io; +pub use std::io::{SeekFrom}; pub use std::iter::repeat_with; pub use std::iter; pub use std::marker::PhantomData; @@ -28,7 +29,7 @@ pub use std::ops::{Deref, DerefMut}; pub use std::os::unix::ffi::OsStrExt; pub use std::os::unix; pub use std::path::PathBuf; -pub use std::process::exit; +pub use std::process::{exit, Command}; pub use std::str::FromStr; pub use std::str; pub use std::string::ParseError; @@ -100,6 +101,7 @@ pub use crate::gamestate::*; pub use crate::global::*; pub use crate::keydata::*; pub use crate::mgmtchannel::*; +pub use crate::nwtemplates; pub use crate::pieces::*; pub use crate::shapelib; pub use crate::slotmap_slot_idx::*; diff --git a/src/lib.rs b/src/lib.rs index b8f51b70..7297a63a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,5 +27,6 @@ pub mod shapelib; pub mod tz; pub mod accounts; pub mod config; +pub mod nwtemplates; #[path="toml-de.rs"] pub mod toml_de; #[path="slotmap-slot-idx.rs"] pub mod slotmap_slot_idx; diff --git a/src/nwtemplates.rs b/src/nwtemplates.rs index 5005aa2e..393d1480 100644 --- a/src/nwtemplates.rs +++ b/src/nwtemplates.rs @@ -2,9 +2,15 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // There is NO WARRANTY. -use parking_lot::RwLock; +use crate::imports::*; -static STATE : RwLock> = const_mutex(None); +use parking_lot::{const_rwlock, RwLock, MappedRwLockReadGuard}; + +static STATE : RwLock> = const_rwlock(None); + +struct State { + tera: tera::Tera, +} #[throws(StartupError)] pub fn init() { @@ -12,13 +18,13 @@ pub fn init() { assert!(guard.is_none()); let glob = format!("{}/*.tera", config().nwtemplates); *guard = State { - tera: tera::new(&glob)?, + tera: tera::Tera::new(&glob)?, }; } #[throws(tera::Error)] -pub fn template_render(template_name: &str, data: &D) { - fn get_st() -> MappedRwLockReadGuard { +pub fn render(template_name: &str, data: &D) { + fn get_st() -> MappedRwLockReadGuard<'static, State> { STATE.read().as_ref().unwrap() } get_st().render(template_name, data) diff --git a/src/spec.rs b/src/spec.rs index 2115d1c9..3afb86e4 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -119,7 +119,7 @@ pub struct FixedToken { pub token: RawToken } #[derive(Debug,Serialize,Deserialize)] pub struct TokenByEmail { /// RFC822 recipient field syntax (therefore, ASCII) - pub addr: Sring, + pub addr: String, } #[derive(Debug,Serialize,Deserialize)] @@ -331,6 +331,7 @@ pub mod implementation { fn check_spec_permission(&self, _: Option) { } fn deliver(&self, + g: &Instance, gpl: &GPlayerState, ipl: &IPlayerState, token: AccessTokenInfo) @@ -345,6 +346,7 @@ pub mod implementation { impl PlayerAccessSpec for PlayerAccessUnset { #[throws(TokenDeliveryError)] fn deliver(&self, + _g: &Instance, _gpl: &GPlayerState, _ipl: &IPlayerState, _token: AccessTokenInfo) -> AccessTokenReport { @@ -366,6 +368,7 @@ pub mod implementation { } #[throws(TokenDeliveryError)] fn deliver(&self, + _g: &Instance, _gpl: &GPlayerState, _ipl: &IPlayerState, _token: AccessTokenInfo) -> AccessTokenReport { @@ -377,6 +380,7 @@ pub mod implementation { impl PlayerAccessSpec for UrlOnStdout { #[throws(TDE)] fn deliver<'t>(&self, + _g: &Instance, _gpl: &GPlayerState, _ipl: &IPlayerState, token: AccessTokenInfo) @@ -394,44 +398,80 @@ pub mod implementation { ipl: &IPlayerState, token: AccessTokenInfo) -> AccessTokenReport { - let messagefile = tempfile::tempfile()?; - - #[derive(Serialize)] + #[derive(Debug,Serialize)] struct CommonData<'r> { player_email: &'r str, - game_name: &'r str, - token_url: &'r str, + game_name: String, nick: &'r str, + token_lines: Vec, }; let common = CommonData { player_email: &self.addr, - game_name: &g.name, - token_url: & + game_name: g.name.to_string(), + token_lines: token.report(), + nick: &gpl.nick, + }; + + if self.addr.find(['\r','\n'] as &[char]).is_some() { + throw!(anyhow!("email address may not contain line endings")); } let message = match &ipl.account { AS::Unix { user } => { - struct Data { - pub gname: - } - - - write!(&mut message, r#"\ -"#, - &self.addr, &gname, - &ipl.account, &gname, - - - write!(&mut message, r#"\ -! "#, - &self.addr, &gname, - &ipl.account, &gname, - let command = Command::new(&config().sendmail) - .args(&["-oee","-odb","-oi","--"]) - .stdin( - + #[derive(Debug,Serialize)] + struct Data<'r> { + unix_user: &'r str, + #[serde(flatten)] + common: CommonData<'r>, + }; + let data = Data { + unix_user: user, + common, + }; + nwtemplates::render("token-unix", &data) + } + other => { + #[derive(Debug,Serialize)] + struct Data<'r> { + account: String, + #[serde(flatten)] + common: CommonData<'r>, + }; + let data = Data { + account: other.to_string(), + common, + }; + nwtemplates::render("token-other", &data) + } + }.context("render email template")?; + + let messagefile = (||{ + let messagefile = tempfile::tempfile().context("tempfile")?; + messagefile.write_all(message.as_bytes()).context("write")?; + messagefile.flush().context("flush")?; + messagefile.seek(SeekFrom::Start(0)).context("seek")?; + messagefile + })().context("write email to temporary file.")?; + + let sendmail = &config().sendmail; + let command = Command::new(sendmail) + .args(&["-oee","-odb","-oi","-t","--"]) + .stdin(messagefile) + .pre_exec(|| unsafe { + // https://github.com/rust-lang/rust/issues/79731 + let r = libc::dup2(2,1); + assert_eq!(r, 1); + }); + let st = command + .status() + .with_context(!! format!("run sendmail ({})", sendmail))?; + if !st.success() { + throw!(format!("sendmail ({}) failed: {} ({})", sendmail, st, st)); + } - AccessTokenReport { lines: todo!() } + AccessTokenReport { lines: vec![ + "Token sent by email.".to_string() + ]} } } -- 2.30.2