mail_template/
lib.rs

1extern crate failure;
2extern crate serde;
3extern crate futures;
4extern crate mail_core;
5extern crate mail_headers;
6extern crate vec1;
7extern crate toml;
8extern crate maybe_owned;
9#[cfg(feature="handlebars")]
10extern crate handlebars as hbs;
11
12#[cfg(all(feature="handlebars", not(feature="handlebars-bindings")))]
13compile_error!("use feature `handlebars-bindings` instead of opt-dep-auto-feature `handlebars`");
14
15use std::{
16    fs,
17    collections::HashMap,
18    fmt::Debug,
19    path::{Path, PathBuf},
20    ops::Deref
21};
22
23use serde::{
24    Serialize,
25    Deserialize
26};
27use failure::Error;
28use futures::{
29    Future, Poll, Async,
30    try_ready,
31    future::{self, Join, Either}
32};
33use vec1::Vec1;
34use maybe_owned::MaybeOwned;
35
36use mail_core::{
37    Resource,
38    Data, Metadata,
39    Context, ResourceContainerLoadingFuture,
40    compose::{MailParts, BodyPart},
41    Mail
42};
43use mail_headers::{
44    HeaderKind, Header,
45    header_components::MediaType,
46    headers
47};
48
49mod base_dir;
50mod path_rebase;
51mod additional_cid;
52pub mod serde_impl;
53pub mod error;
54
55#[cfg(feature="handlebars")]
56pub mod handlebars;
57
58pub use self::base_dir::*;
59pub use self::path_rebase::*;
60pub use self::additional_cid::*;
61
62/// Trait used to bind/implement template engines.
63pub trait TemplateEngine: Sized {
64    type Id: Debug;
65
66    type LazyBodyTemplate: PathRebaseable + Debug + Send + Serialize + for<'a> Deserialize<'a>;
67
68    fn load_body_template(&mut self, tmpl: Self::LazyBodyTemplate)
69        -> Result<BodyTemplate<Self>, Error>;
70
71    fn load_subject_template(&mut self, template_string: String)
72        -> Result<Self::Id, Error>;
73}
74
75/// Additional trait a template engine needs to implement for the types it can process as input.
76///
77/// This could for example be implemented in a wild card impl for the template engine for
78/// any data `D` which implements `Serialize`.
79pub trait TemplateEngineCanHandleData<D>: TemplateEngine {
80
81    fn render<'r, 'a>(
82        &'r self,
83        id: &'r Self::Id,
84        data: &'r D,
85        additional_cids: AdditionalCIds<'r>
86    ) -> Result<String, Error>;
87}
88
89/// Load a template as described in a toml file.
90///
91/// This will set the default of the base_dir to the
92/// dir the template file loaded is in.
93pub fn load_toml_template_from_path<TE, C>(
94    engine: TE,
95    path: PathBuf,
96    ctx: &C
97) -> impl Future<Item=Template<TE>, Error=Error>
98    where TE: TemplateEngine + 'static, C: Context
99{
100
101    let ctx2 = ctx.clone();
102    ctx.offload_fn(move || {
103        let content = fs::read_to_string(&path)?;
104        let base: serde_impl::TemplateBase<TE> = toml::from_str(&content)?;
105        let base_dir = path.parent().unwrap_or_else(||Path::new("."));
106        let base_dir = CwdBaseDir::from_path(base_dir)?;
107        Ok((base, base_dir))
108    }).and_then(move |(base, base_dir)| base.load(engine, base_dir, &ctx2))
109}
110
111/// Load a template as described in a toml string;
112pub fn load_toml_template_from_str<TE, C>(
113    engine: TE,
114    content: &str,
115    ctx: &C
116) -> impl Future<Item=Template<TE>, Error=Error>
117    where TE: TemplateEngine, C: Context
118{
119    let base: serde_impl::TemplateBase<TE> =
120        match toml::from_str(content) {
121            Ok(base) => base,
122            Err(err) => { return Either::B(future::err(Error::from(err))); }
123        };
124
125    let base_dir =
126        match CwdBaseDir::from_path(Path::new(".")) {
127            Ok(base_dir) => base_dir,
128            Err(err) => { return Either::B(future::err(Error::from(err))) }
129        };
130
131    Either::A(base.load(engine, base_dir, ctx))
132}
133
134
135/// A Mail template.
136#[derive(Debug)]
137pub struct Template<TE: TemplateEngine> {
138    template_name: String,
139    base_dir: CwdBaseDir,
140    subject: Subject<TE>,
141    /// This can only be in the loaded form _iff_ this is coupled
142    /// with a template engine instance, as using it with the wrong
143    /// template engine will lead to potential bugs and panics.
144    bodies: Vec1<BodyTemplate<TE>>,
145    //TODO: make sure
146    embeddings: HashMap<String, Resource>,
147    attachments: Vec<Resource>,
148    engine: TE,
149}
150
151impl<TE> Template<TE>
152    where TE: TemplateEngine
153{
154    pub fn inline_embeddings(&self) -> &HashMap<String, Resource> {
155        &self.embeddings
156    }
157
158    pub fn attachments(&self) -> &[Resource] {
159        &self.attachments
160    }
161
162    pub fn engine(&self) -> &TE {
163        &self.engine
164    }
165
166    pub fn bodies(&self) -> &[BodyTemplate<TE>] {
167        &self.bodies
168    }
169
170    pub fn subject_template_id(&self) -> &TE::Id {
171        &self.subject.template_id
172    }
173}
174
175
176/// Represents one of potentially many alternate bodies in a template.
177#[derive(Debug)]
178pub struct BodyTemplate<TE: TemplateEngine> {
179    pub template_id: TE::Id,
180    pub media_type: MediaType,
181    pub inline_embeddings: HashMap<String, Resource>
182}
183
184impl<TE> BodyTemplate<TE>
185    where TE: TemplateEngine
186{
187    pub fn template_id(&self) -> &TE::Id {
188        &self.template_id
189    }
190
191    pub fn media_type(&self) -> &MediaType {
192        &self.media_type
193    }
194
195    pub fn inline_embeddings(&self) -> &HashMap<String, Resource> {
196        &self.inline_embeddings
197    }
198}
199
200/// Represents a template used for generating the subject of a mail.
201#[derive(Debug)]
202pub struct Subject<TE: TemplateEngine> {
203    template_id: TE::Id
204}
205
206impl<TE> Subject<TE>
207    where TE: TemplateEngine
208{
209    pub fn template_id(&self) -> &TE::Id {
210        &self.template_id
211    }
212}
213
214/// Automatically provides the `prepare_to_render` method for all `Templates`
215///
216/// This trait is implemented for all `Templates`/`D`(data) combinations where
217/// the templates template engine can handle the given data (impl. `TemplateEngineCanHandleData<D>`)
218///
219/// This trait should not be implemented by hand.
220pub trait TemplateExt<TE, D>
221    where TE: TemplateEngine + TemplateEngineCanHandleData<D>
222{
223
224    fn render_to_mail_parts<'r>(&self, data: LoadedTemplateData<'r, D>, ctx: &impl Context)
225        -> Result<(MailParts, Header<headers::Subject>), Error>;
226
227    fn render<'r>(&self, data: LoadedTemplateData<'r, D>, ctx: &impl Context) -> Result<Mail, Error> {
228        let (parts, subject) = self.render_to_mail_parts(data, ctx)?;
229        let mut mail = parts.compose();
230        mail.insert_header(subject);
231        Ok(mail)
232    }
233}
234
235
236impl<TE, D> TemplateExt<TE, D> for Template<TE>
237    where TE: TemplateEngine + TemplateEngineCanHandleData<D>
238{
239    fn render_to_mail_parts<'r>(&self, data: LoadedTemplateData<'r, D>, ctx: &impl Context)
240        -> Result<(MailParts, Header<headers::Subject>), Error>
241    {
242        let TemplateData {
243            data,
244            inline_embeddings,
245            mut attachments
246        } = data.into();
247
248        let subject = self.engine().render(
249            self.subject_template_id(),
250            &data,
251            AdditionalCIds::new(&[])
252        )?;
253
254        let subject = headers::Subject::auto_body(subject)?;
255
256        //TODO use Vec1 try_map instead of loop
257        let mut bodies = Vec::new();
258        for body in self.bodies().iter() {
259            let raw = self.engine().render(
260                body.template_id(),
261                &data,
262                AdditionalCIds::new(&[
263                    &inline_embeddings,
264                    body.inline_embeddings(),
265                    self.inline_embeddings()
266                ])
267            )?;
268
269            let data = Data::new(
270                raw.into_bytes(),
271                Metadata {
272                    file_meta: Default::default(),
273                    media_type: body.media_type().clone(),
274                    content_id: ctx.generate_content_id()
275                }
276            );
277
278            let inline_embeddings = body.inline_embeddings()
279                .values()
280                .cloned()
281                .collect();
282
283            bodies.push(BodyPart {
284                resource: Resource::Data(data),
285                inline_embeddings,
286                attachments: Vec::new()
287            });
288        }
289
290        attachments.extend(self.attachments().iter().cloned());
291
292        let mut inline_embeddings_vec = Vec::new();
293        for (key, val) in self.inline_embeddings() {
294            if !inline_embeddings.contains_key(key) {
295                inline_embeddings_vec.push(val.clone())
296            }
297        }
298
299        inline_embeddings_vec.extend(inline_embeddings.into_iter().map(|(_,v)|v));
300
301        let parts = MailParts {
302            //UNWRAP_SAFE (complexly mapping a Vec1 is safe)
303            alternative_bodies: Vec1::try_from_vec(bodies).unwrap(),
304            inline_embeddings: inline_embeddings_vec,
305            attachments
306        };
307
308        Ok((parts, subject))
309    }
310}
311
312pub struct TemplateData<'a, D: 'a> {
313    pub data:  MaybeOwned<'a, D>,
314    pub attachments: Vec<Resource>,
315    pub inline_embeddings: HashMap<String, Resource>
316}
317
318impl<'a, D> TemplateData<'a, D> {
319
320    pub fn load(self, ctx: &impl Context) -> DataLoadingFuture<'a, D> {
321        let TemplateData {
322            data,
323            attachments,
324            inline_embeddings
325        } = self;
326
327        let loading_fut = Resource::load_container(inline_embeddings, ctx)
328            .join(Resource::load_container(attachments, ctx));
329
330        DataLoadingFuture {
331            payload: Some(data),
332            loading_fut
333        }
334    }
335}
336impl<D> From<D> for TemplateData<'static, D> {
337    fn from(data: D) -> Self {
338        TemplateData {
339            data: data.into(),
340            attachments: Default::default(),
341            inline_embeddings: Default::default()
342        }
343    }
344}
345
346impl<'a, D> From<&'a D> for TemplateData<'a, D> {
347    fn from(data: &'a D) -> Self {
348        TemplateData {
349            data: data.into(),
350            attachments: Default::default(),
351            inline_embeddings: Default::default()
352        }
353    }
354}
355
356pub struct LoadedTemplateData<'a, D: 'a>(TemplateData<'a, D>);
357
358impl<'a, D> From<&'a D> for LoadedTemplateData<'a, D> {
359    fn from(data: &'a D) -> Self {
360        LoadedTemplateData(TemplateData::from(data))
361    }
362}
363
364impl<D> From<D> for LoadedTemplateData<'static, D> {
365    fn from(data: D) -> Self {
366        LoadedTemplateData(TemplateData::from(data))
367    }
368}
369
370impl<'a, D> Deref for LoadedTemplateData<'a, D> {
371    type Target = TemplateData<'a, D>;
372
373    fn deref(&self) -> &Self::Target {
374        &self.0
375    }
376}
377
378impl<'a, D> Into<TemplateData<'a, D>> for LoadedTemplateData<'a, D> {
379    fn into(self) -> TemplateData<'a, D> {
380        let LoadedTemplateData(data) = self;
381        data
382    }
383}
384
385/// Future returned when preparing a template for rendering.
386pub struct DataLoadingFuture<'a, D: 'a> {
387    payload: Option<MaybeOwned<'a, D>>,
388    loading_fut: Join<
389        ResourceContainerLoadingFuture<HashMap<String, Resource>>,
390        ResourceContainerLoadingFuture<Vec<Resource>>
391    >
392}
393
394impl<'a, D> Future for DataLoadingFuture<'a, D> {
395    type Item = LoadedTemplateData<'a, D>;
396    type Error = Error;
397
398    fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
399        let (
400            inline_embeddings,
401            attachments
402        ) = try_ready!(self.loading_fut.poll());
403
404        //UNWRAP_SAFE only non if polled after resolved
405        let data = self.payload.take().unwrap();
406
407        let inner = TemplateData {
408            data,
409            inline_embeddings,
410            attachments
411        };
412
413        Ok(Async::Ready(LoadedTemplateData(inner)))
414    }
415}