chiark / gitweb /
wip email templates etc.
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 5 Dec 2020 14:16:44 +0000 (14:16 +0000)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 5 Dec 2020 14:16:44 +0000 (14:16 +0000)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
nwtemplates/token-other.tera
nwtemplates/token-unix.tera
src/error.rs
src/global.rs
src/imports.rs
src/lib.rs
src/nwtemplates.rs
src/spec.rs

index 99b197144f38437664fc6019ab68569f260123ab..5c3441e745617f8e9b37e454cb2a215eea85d34e 100644 (file)
@@ -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 }}
index dacbb4d8b51e960a9a741e025efba30df5399dcd..f764d2dc11039b3d80512100e21b3688adbe6460 100644 (file)
@@ -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 }}
index 47ccdf9884cfb6b2723a771f8649568d0a602061..4ae9748c657f6a75f6411dce837d4353637b3cd2 100644 (file)
@@ -56,7 +56,17 @@ pub enum InternalError {
 }
 
 #[derive(Clone,Error,Debug,Serialize,Deserialize)]
-pub enum TokenDeliveryError {
+#[error("{0}")]
+pub struct TokenDeliveryError(String);
+impl From<anyhow::Error> for TokenDeliveryError {
+  fn from(a: anyhow::Error) -> TokenDeliveryError {
+    TokenDeliveryError(
+      a.chain()
+        .map(ToString::to_string)
+        .collect::<Vec<_>>()
+        .join(": ")
+    )
+  }
 }
 
 impl From<InternalError> for SpecError {
index 9cacd9d418665e0e4b819adf3b598b2ff20dac47..6faca941f59817cfcd7224f1babd9aae7fad6100 100644 (file)
@@ -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
   }
 
index 783d5738d312f127755b8c9139871979b228a156..8206ee6a844099a3c0bcb5a4d345e98c2a089bc8 100644 (file)
@@ -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::*;
index b8f51b70d0324928e9a8ffe008bd22c46096cb6c..7297a63a075bdabc6bdbeb8d4990b6a67193c5f4 100644 (file)
@@ -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;
index 5005aa2eccfe61d353affcfc41408d463b111934..393d1480c2bb96c307696f3dac54851141ba44f5 100644 (file)
@@ -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<Option<State>> = const_mutex(None);
+use parking_lot::{const_rwlock, RwLock, MappedRwLockReadGuard};
+
+static STATE : RwLock<Option<State>> = 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<D: Serialize>(template_name: &str, data: &D) {
-  fn get_st() -> MappedRwLockReadGuard<State> {
+pub fn render<D: Serialize>(template_name: &str, data: &D) {
+  fn get_st() -> MappedRwLockReadGuard<'static, State> {
     STATE.read().as_ref().unwrap()
   }
   get_st().render(template_name, data)
index 2115d1c95eee54dd5f672b7b807bd08d1503cff9..3afb86e457f70c94648cabd4302c363e084d1521 100644 (file)
@@ -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<AuthorisationSuperuser>) {
     }
     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<String>,
       };
       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()
+      ]}
     }
   }