summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilipp Korber <philippkorber@gmail.com>2018-11-22 14:24:07 +0100
committerPhilipp Korber <philippkorber@gmail.com>2018-11-22 14:24:07 +0100
commit9287ce6d3e6a82b8cffb1ebb579d3a84af76e20d (patch)
tree6d2b7b6d10bb242ec6cff1affe70f1e717dda54d
parent3edb27a69d928df5077cd8e0cf6f9f05278a1a7c (diff)
parentce63ad36c703dad8f0639f9c53c9197419dec95c (diff)
Merged `mail-template` repository (through the new-api branch)
-rw-r--r--.vscode/settings.json9
-rw-r--r--template/Cargo.toml39
-rw-r--r--template/README.md126
-rw-r--r--template/notes/notes.md331
-rw-r--r--template/src/additional_cid.rs60
-rw-r--r--template/src/base_dir.rs153
-rw-r--r--template/src/handlebars.rs96
-rw-r--r--template/src/lib.rs411
-rw-r--r--template/src/path_rebase.rs191
-rw-r--r--template/src/serde_impl.rs447
10 files changed, 1863 insertions, 0 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..ab9b2b2
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,9 @@
+{
+ "cSpell.words": [
+ "Deserializer",
+ "Serializer",
+ "deserialize",
+ "embeddings",
+ "serde"
+ ]
+} \ No newline at end of file
diff --git a/template/Cargo.toml b/template/Cargo.toml
new file mode 100644
index 0000000..18ef13f
--- /dev/null
+++ b/template/Cargo.toml
@@ -0,0 +1,39 @@
+[package]
+name = "mail-template"
+version = "0.2.0"
+description = "[mail] provides a way to create bind string template engines to produce mails"
+authors = ["Philipp Korber <p.korber@1aim.com>"]
+keywords = ["mail-api", "template"]
+categories = []
+license = "MIT OR Apache-2.0"
+readme = "./README.md"
+documentation = "https://docs.rs/mail-template"
+repository = "https://github.com/1aim/mail-template"
+
+[features]
+default = []
+handlebars-bindings = ["handlebars"]
+
+[dependencies]
+mail-core = { git="https://github.com/1aim/mail", features=["serde-impl"] }
+mail-internals = { git="https://github.com/1aim/mail" }
+mail-headers = { git="https://github.com/1aim/mail", features=["serde-impl"] }
+
+failure = "0.1.1"
+futures = "0.1.14"
+vec1 = { version="1.1", features=["serde"]}
+soft-ascii-string = "1.0"
+serde = { version="1", features=["derive"] }
+toml = "0.4.8"
+maybe-owned = "0.3.2"
+
+handlebars = { version="1.1.0", optional=true }
+
+[dependencies.mime]
+git="https://github.com/1aim/mime"
+branch="parser_revamp"
+version="0.4.0"
+
+
+[dev-dependencies]
+
diff --git a/template/README.md b/template/README.md
new file mode 100644
index 0000000..104c9a8
--- /dev/null
+++ b/template/README.md
@@ -0,0 +1,126 @@
+
+# mail-template
+
+**Provides mechanisms for generating mails based on templates**
+
+---
+
+This crate provides a general interface for using template engine with the mail crate.
+
+It's core is the `TemplateEngine` trait which can be implemented to bind a template engine.
+When rendering a template the template engine implementing the `TemplateEngine` trait will
+produce a number of (wrapped) `Resource` instances representing the alternate bodies of amail as well as a number of additional `Resources` used for embedded content (e.g. logoimages) and attachments. This crate then takes this parts and composes a multipart mime mail from
+it.
+
+## Template Engine implementations
+
+A mail template engine has to do more then just taking a single text
+template (e.g. a handlebars template) and produce some output using
+string magic. It has to:
+
+1. consider alternate bodies, so it should render at last two
+ "text templates" (one text/plain, one html)
+
+2. consider which additional embeddings/attachments should be included
+ (all in the given template data are included, but you might
+ add additional ones, e.g. some logo image)
+
+As such text template engines like `Handle` bar can not directly
+be bound to the `TemplateEngine` trait.
+
+For using text template engine it's recommended to use
+the `mail-template-render-engine` (also exposed through the
+mail facade) which implements this overhead for any engine
+which can "just" render some text and provides default
+bindings to some common template engines (e.g. Handlebars).
+
+## Derive
+
+This crate requires template data to implement `InspectEmbeddedResources`
+which combined with some typing/generic design decisions allows to bind
+not just to template engines which use serialization to access template
+data but also to such which use static typing (like `askama`).
+
+As such it re-exports the `InspectEmbeddedResources` derive from
+`mail-derive`. Note that if you use the mail facade it also does
+re-export the derive.
+
+## Features
+
+- `askama-engine`, includes bindings for the askama template engine.
+- `serialize-to-content-id`, implements Serialize for `Embedded`,
+ `EmbeddedWithCId` which serializes the embedded type **into its
+ content id**. E.g. a image with content id `"q09cu3@example.com"`
+ will be serialized to the string `"q09cu3@example.com"`. This is
+ extremely useful for all template engines which use serialization
+ as their way to access template data.
+
+## Example
+
+```rust
+```
+
+## Road Map
+
+The current implementation has a number of limitations which should be lifted with
+future versions:
+
+- Only a limited subset of headers are/can be set through the template engine
+ (`Sender`, `From`, `To`, `Subject`) while some headers are set implicitly
+ when encoding the mail (e.g. `Date`, `Content-Type`, `Content-Disposition`).
+ But sometimes it would be useful to add some custom headers through the template
+ engine (both on the outermost and inner bodies).
+- `From`, `To`, `Subject` have to be given, but sometimes you might want to just
+ create the `Mail` type and then set them by yourself (through you _can_ currently
+ override them)
+- Re-use/integration of existing mail instances: Some times you might want to
+ use a `Mail` instance created some where else as a body for a multipart mail
+ generated from a template (e.g. some thing generating "special" attachments).
+
+Also there are some parts which are likely to change:
+
+- `MailSendData`/`MailSendDataBuilder` the name is
+ not very good it also needs to change to handle
+ the thinks listed above
+- `Embedded`, `EmbeddedWithCid`, embeddings and attachments
+ currently a `Embedded` instance is a wrapper around `Resource`
+ representing something which will become a mail body but is not
+ a main body (i.e. it not the text/html/.. you send) instead it
+ something embedded in the mail which is either used as attachment
+ or as a embedding (e.g. a logo image). Through the content disposition
+ the `Embedded` instance differs between thing embedded and internally
+ used or embedded and used as attachment at the same time many different
+ arrays are sometimes used to differ between them (e.g. in `MailParts`)
+ but there is no (type system) check to make sure a array of thinks used
+ as attachments can not contain a `Embedded` instance with content disposition
+ inline. The result will still be a valid mail, but likely not in the
+ way you expect it to be. This should be fixed one way or another (making
+ the different array type safe and lifting disposition to the type level
+ had been used but didn't play out nicely).
+- `serialize-to-content-id`, is a nice idea but has some problems in
+ some edge cases (e.g. when you want to serialize anything containing
+ a `Embedded` type for any usage BUT accessing it in an template). So
+ it might be removed, which means a import like `cid:{{data.mything}}`
+ (using some mustach pseudo template syntax) would become `cid:{{data.mything.cid}}`.
+
+
+## Documentation
+
+
+Documentation can be [viewed on docs.rs](https://docs.rs/mail-template).
+(once published)
+
+## License
+
+Licensed under either of
+
+* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
+* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
+
+at your option.
+
+### Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted
+for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
+additional terms or conditions.
diff --git a/template/notes/notes.md b/template/notes/notes.md
new file mode 100644
index 0000000..a6d68ea
--- /dev/null
+++ b/template/notes/notes.md
@@ -0,0 +1,331 @@
+
+# Outer Most interface
+
+something like a Mailer which might implement tokio_servie::Service (if
+so multiple parameters are wrapped into a tupple)
+
+mailer contains information like `from`
+
+`mailer.send_mails( recipients_data, mail_gen )`
+
+where recipients_data is a iterable mapping from address to recipient specific data,
+e.g. `Vec<(Address, Data)>`
+
+and mail_gen is something like `trait MailGen { fn gen_mail( from, to, data, bits8support ) -> MailBody; }`
+
+`MailBody` is not `tokio_smtp::MailBody` but has to implement nessesray contraints,
+(e.g. implemnting `toki_smtp::IntoMailBody` not that for the beginning this will be
+hard encoded but later one a generic variation allowing `smtp` to be switched out
+by something else is also possible`)
+
+MailGen implementations are not done by hand but implemented ontop of something
+like a template spec e.g. `struct TemplateSpec { id_template: TemplateId, additional_appendixes: Vec<Appendix> }`
+
+Where `TemplateId` can is e.g. `reset_link` leading to the creation of a `html` with alternate `plain`
+mail iff there is a `reset_link.html` and a `reset_link.plain` template. A `reset_link.html.data`
+folder could be used to define inline (mime related) appendixes like embedded images,
+but we might want to have a way to define such embeddigns through the data (
+E.g. by mapping `Data => TemplateEnginData` and replacing `EmbeddedFile` variations
+by a new related id and adding the `EmbeddedFile(data)` data to the list of embeddings)
+
+
+
+# List of parts possible non-ascii and not ascii encodable
+
+- local-part (address/addr-spec/local-part)
+
+# Limitations
+
+Line length limit:
+
+SHOULD be no more than 78 chars (excluding CRLF!)
+MUST NOT be more than 998 chars (excluding CRLF)
+
+# Orphan `\n`,`\r`
+
+MUST NOT occur in header (except for folding)
+MUST NOT occur in body (except for newline)
+
+## Header specific limitations
+
+- encoded word max length of 75 chars
+- spaces around encoed words are ignored??
+
+
+# Email Address part (a@b.e)
+
+- there is a `domain-literal` version which does use somthing like `[some_thing]`,
+ we can use puny code for converting domains into ascii but probably can't use
+ this with `domain-literal`'s
+
+- `local-part` is `dot-atom` which has leading and trailing `[CFWS]` so comments are alowed
+
+- MessageId uses a email address like syntax but without supporting spaces/comments
+
+
+# MIME
+
+fields containing mime types can have parameters with a `<type>; key=value` style
+this is mainly used for `multipart/mixed; boundary=blablabla` and similar.
+
+You have to make sure the boundary does not appear in any of the "sub-bodies",
+this is kinda easy for bodies with e.g. content transfer encoding Base64,
+but can be tricky in combination with some other content as normal text
+can totally contain the boundary. To prevent this:
+
+- use long boundary strings
+- encode the body with base64 even if it's "just" ascii
+ - OR check the content and encode parts of it if necessary
+
+you can have multipart in multipart creating a tree,
+make sure you don't mix up the boundaries
+
+
+A body part does not have to have any headers, assume default values if
+there is no header, bodies which have no header _have to start with a
+blank line_ separating 0 headers from the body.
+
+Header fields of bodies which do not start with `Content-` _are ignored_!
+
+Contend types:
+
+- `mixed`, list of sub-bodies with mixed mime types, might be displayed inline or as appendix
+ - use >>`Content-Disposition` (RFC 2183)<< to controll this, even through it's not standarized yet (or is it by now?)
+ - default body mime type is `text/plain`
+- `digest` for combining muliple messages of content type `message/rfc822`
+ - e.g. `(multipar/mixed ("table of content") (multipart/digest "message1", "message2"))`
+ - `message` (mainly `message/rfc822`) contains _another_ email, e.g. for digest
+ - wait is there a `multipart/message`?? proably not!
+- `alternative` multiple alternative versions of the "same" information
+ - e.g. `(multipart/alternative (text/plain ...) (text/html ...))`
+ - _place preferred form last!_ (i.e. increasing order of preference)
+ - interesting usage with `application/X-FixedRecord`+`application/octet-stream`
+- `related` (RFC 2387) all bodies are part of one howl, making no (less) sense if placed alone
+ - the first part is normally the entry point, but this can be chaged through parameters
+ - (only relevant for parsing AND interpreting it, but not for generating as we can always use the default)
+ - Content-ID is used to specify a id on each body respectivly which can be used to refer to it (e.g. in HTML)
+ - in html use e.g. `<img src="cid:the_content_id@goes.here>....</img>`
+ - example is `(multipart/relat (text/html ...) (image/jpeg (Content-ID <bala@bal.bla>) ...))` for embedding a image INTO a HTML mail
+- `report`
+- `signed` (body part + signature part)
+- `encrypted` (encryption information part + encrypted data (`application/octet-stream`))
+- `form-data`
+- `x-mixed-replace` (for server push, don't use by now there are better ways)
+- `byteranges`
+
+
+Example mail structure:
+
+```
+(multipart/mixed
+ (multipart/alternative
+ (text/plain ... )
+ (multipart/related
+ (text/hmtl ... '<img src="cid:ContentId@1aim.com"></img>' ... )
+ (image/png (Content-ID <ContentId@1aim.com>) ... )
+ ... ))
+ (image/png (Content-Disposition attachment) ...)
+ (image/png (Content-Disposition attachment) ...))
+```
+
+Possible alternate structure:
+
+```
+(multipart/mixed
+ (multipart/related
+
+ (multipart/alternative
+ (text/plain ... '[cid:ContentId@1aim.com]' ... )
+ (text/html ... '<img src="cid:ContentId@1aim.com"></img>' ... ) )
+
+ (image/png (Content-ID <ContentId@1aim.com>) ... ) )
+
+ (image/png (Content-Disposition attachment) ...)
+ (image/png (Content-Disposition attachment) ...))
+```
+
+but I have not seen the `[cid:...]` for text/plain in any standard, through it might be there.
+Also if se we might still have a related specific for the html (for html only stuff) so:
+- place Embedding in Data in the outer `multipart/related`
+- place Embedding returned by the template in inner `multipart/related`
+
+# Attatchment
+
+proposed filenames for attachments can be given through parameters of the disposition header
+
+it does not allow non ascii character there!
+
+see rfc2231 for more information, it extends some part wrt.:
+
+- splitting long parameters (e.g. long file names)
+- specifying language and character set
+- specifying language for encoded words
+
+# Encoded Words
+
+extended by rfc2231
+
+additional limits in header fields
+
+header containing encoded words are limited to 76 bytes
+
+a "big" text chunk can be split in multiple encoded words seperated by b'\r\n '
+
+non encoded words and encoded words can apear in the same header field, but
+must be seperate by "linear-white-space" (space) which is NOT removed when
+decoding encoded words
+
+encoded words can appear in:
+
+- `text` sections where `text` is based on RFC 822! (e.g. Content-Description )
+ - in context of RFC 5322 this means `unstructured` count's as text
+- `comments` (as alternative to `ctext`,`quoted-pair`,`comment`
+- `word`'s within a `phrase`
+
+**Therefor it MUST NOT appear in any structured header field except withing a `comment` or `phrase`!**
+
+**You have to encode text which looks like an encoded word**
+
+
+
+limitations:
+
+- in comment's no ')',')' and '"'
+- in headers no ' '
+
+
+# Other
+
+there is no `[CFWS]` after the `:` in Header fields,
+but most (all?) of the parts following them are allowed
+to start with a `[CFWS]`. (exception is unstructured where
+a `CFWS` can be allowed but also MIGHT be part of the
+string)
+
+CFWS -> (un-) foldable whitespace allowing comments
+FWS -> (un-) foldable whitespace without comments
+
+
+# Relevant RFCs
+5321, 5322, 6854, 3492, 2045, 2046, 2047, 4288, 4289, 2049, 6531, 5890
+
+make sure to not use the outdated versions
+
+
+# Parsing Notes
+
+be strict when parsing (e.g. only ws and printable in subject line)
+
+if "some other" strings should still be supported do not do zero
+copy, but instead add the data to a new buff _replacing invalid
+chars with replacement symbol or just stripping them_
+
+
+# Non-utf8 Non-Ascci bytes in Mail body
+
+The mail body can contain non-utf8, non-ascii data (e.g.
+utf16 data, images etc.) WITHOUT base64 encoding if
+8BITMIME is supported (note there is also BINARY and CHUNKING)
+
+smtp still considers _the bytes_ corresponding to CR LF and DOT special.
+
+- there is a line length limit, lines terminate with b'CRLF'
+- b'.CRLF' does sill end the body (if preceeded by CRLF, or body starts with it)
+ - so dot-staching is still done on protocol level
+
+
+
+## Hot to handle `obs-` parsings
+
+we have to be able to parse mails with obsolete syntax (theoretically)
+but should never genrate such mails, the encder excepts its underlying
+data to be correct, but it might not be if we directly place `obs-`
+parsed data there. For many parts this is no problem as the
+`obs-` syntax is a lot about having FWS at other positions,
+_between components_ (so we won't have a problem with the encoder).
+Or some additional obsolete infromations (which we often/allways can just
+"skip" over). So we have to check if there are any braking cases and if
+we have to not zero copy them when parsing but instead transform them
+into a valide representation, in worst case we could add a `not_encodable`
+field to some structs.
+
+# TODO
+check if some parts are empty and error if encode is called on them
+e.g. empty domain
+
+make sure trace and resend fields are:
+
+1. encoded in order (MUST)
+2. encoded as blocks (MUST?)
+3. encoded before other fields (SHOULD)
+
+as people may come up with their own trace like fileds,
+rule 1 and 2 should appy to all fields
+
+
+make sure trace,resent-* are multi fields
+
+add a RawUnstructured not doing any encoding, but only validity checking
+
+# Postponded
+
+`component::Disposition` should have a `Other` variant, using `Token` (which
+means a general extension token type is needed)
+
+other features like signature, encryption etc.
+
+check what happens if I "execute" a async/mio/>tokio<
+based future in a CPU pool? Does it just do live
+polling in the thread? Or does it act more intelligent?
+or does it simply fail?
+
+just before encoding singlepart bodies, resource is resolved,
+therefore:
+
+1. we now have the MediaType + File meta + TransferEncoding
+2. add* ContentType header to headers
+3. add* ContentTransferEncoding header to headers
+4. add* file meta infor to ContentDisposition header if it exists
+5. note that >add*< is not modifying Mail, but adds it to the list of headers to encode
+
+
+warn when encoding a Disposition of kind Attachment which's
+file_meta has no name set
+
+
+// From RFC 2183:
+// NOTE ON PARAMETER VALUE LENGHTS: A short (length <= 78 characters)
+// parameter value containing only non-`tspecials' characters SHOULD be
+// represented as a single `token'. A short parameter value containing
+// only ASCII characters, but including `tspecials' characters, SHOULD
+// be represented as `quoted-string'. Parameter values longer than 78
+// characters, or which contain non-ASCII characters, MUST be encoded as
+// specified in [RFC 2184].
+provide a gnneral way for encoding header parameter which follow the scheme:
+`<mainvalue> *(";" <key>"="<value> )` this are ContentType and ContentDisposition
+ for now
+
+
+IF Item::Encoded only appears as encoded word, make it Item::Encoded word,
+possible checking for "more" validity then noew
+
+
+email::quote => do not escape WSP, and use FWS when encoding
+also make quote, generally available for library useers a
+create_quoted_string( .. )
+
+# Dependencies
+
+quoted_printable and base64 have some problems:
+1. it's speaking of a 76 character limit where it is 78
+ it seems they treated the RFC as 78 character including
+ CRLF where the RFC speaks of 78 characters EXCLUDING
+ CRLF
+2. it's only suited for content transfer encoding the body
+ as there is a limit of the length of encoded words (75)
+ which can't be handled by both
+
+also quoted_printable has another problem:
+3. in headers the number of character which can be displayed without
+ encoding is more limited (e.g. no ' ' ) quoted_printable does not
+ respect this? (TODO CHECK THIS)
diff --git a/template/src/additional_cid.rs b/template/src/additional_cid.rs
new file mode 100644
index 0000000..eecf2e7
--- /dev/null
+++ b/template/src/additional_cid.rs
@@ -0,0 +1,60 @@
+use std::collections::{
+ HashMap, HashSet
+};
+
+use serde::{Serialize, Serializer};
+
+use mail_core::Resource;
+use mail_headers::header_components::ContentId;
+
+pub struct AdditionalCIds<'a> {
+ additional_resources: &'a [&'a HashMap<String, Resource>]
+}
+
+impl<'a> AdditionalCIds<'a> {
+
+ /// Creates a new `AdditionalCIds` instance.
+ ///
+ /// All resources in the all hash maps have to be loaded to the
+ /// `Data` or `EncData` variants or using `get` can panic.
+ pub(crate) fn new(additional_resources: &'a [&'a HashMap<String, Resource>]) -> Self {
+ AdditionalCIds { additional_resources }
+ }
+
+
+ /// Returns the content id associated with the given name.
+ ///
+ /// If multiple of the maps used to create this type contain the
+ /// key the first match is returned and all later ones are ignored.
+ ///
+ /// # Panic
+ ///
+ /// If the resource exists but is not loaded (i.e. has no content id)
+ /// this will panic as this can only happen if there is a bug in the
+ /// mail code, or this type was used externally.
+ pub fn get(&self, name: &str) -> Option<&ContentId> {
+ for possible_source in self.additional_resources {
+ if let Some(res) = possible_source.get(name) {
+ return Some(res.content_id().expect("all resources should be loaded/have a content id"));
+ }
+ }
+ return None;
+ }
+}
+
+impl<'a> Serialize for AdditionalCIds<'a> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where S: Serializer
+ {
+ let mut existing_keys = HashSet::new();
+ serializer.collect_map(
+ self.additional_resources
+ .iter()
+ .flat_map(|m| m.iter().map(|(k, resc)| {
+ (k, resc.content_id().expect("all resources should be loaded/have a content id"))
+ }))
+ .filter(|key| existing_keys.insert(key.to_owned()))
+ )
+ }
+}
+
diff --git a/template/src/base_dir.rs b/template/src/base_dir.rs
new file mode 100644
index 0000000..91e824a
--- /dev/null
+++ b/template/src/base_dir.rs
@@ -0,0 +1,153 @@
+use std::{
+ path::{Path, PathBuf},
+ ops::{Deref, DerefMut},
+ env, io
+};
+
+use serde::{
+ ser::{Serialize, Serializer},
+ de::{Deserialize, Deserializer}
+};
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct CwdBaseDir(PathBuf);
+
+impl CwdBaseDir {
+
+ /// Creates a new `CwdBaseDir` instance containing exactly the given path.
+ pub fn new_unchanged(path: PathBuf) -> Self {
+ CwdBaseDir(path)
+ }
+
+ /// Creates a `CwdBaseDir` from a path by prefixing the path with the
+ /// current working dir if it's relative.
+ ///
+ /// If the path is not relative it's directly used.
+ ///
+ /// # Os state side effects
+ ///
+ /// As this function accesses the current working directory (CWD) it's
+ /// not pure as the CWD can be changed (e.g. by `std::env::set_current_dir`).
+ ///
+ /// # Error
+ ///
+ /// As getting the CWD can fail this function can fail with a I/O Error, too.
+ pub fn from_path<P>(path: P) -> Result<Self, io::Error>
+ where P: AsRef<Path> + Into<PathBuf>
+ {
+ let path =
+ if path.as_ref().is_absolute() {
+ path.into()
+ } else {
+ let mut cwd = env::current_dir()?;
+ cwd.push(path.as_ref());
+ cwd
+ };
+
+ Ok(CwdBaseDir(path))
+ }
+
+ /// Turns this path into a `PathBuf` by stripping the current working dir
+ /// if it starts with it.
+ ///
+ /// If this path does not start with the CWD it's returned directly.
+ ///
+ /// # Os state side effects
+ ///
+ /// As this function used the current working dir (CWD) it is affected
+ /// by any function changing the CWD as a side effect.
+ ///
+ /// # Error
+ ///
+ /// Accessing the current working dir can fail, as such this function
+ /// can fail.
+ pub fn to_base_path(&self) -> Result<&Path, io::Error> {
+ let cwd = env::current_dir()?;
+ self.strip_prefix(&cwd)
+ .or_else(|_err_does_not_has_that_prefix| {
+ Ok(&self)
+ })
+ }
+
+ /// Turns this instance into the `PathBuf` it dereferences to.
+ pub fn into_inner_with_prefix(self) -> PathBuf {
+ let CwdBaseDir(path) = self;
+ path
+ }
+}
+
+impl Deref for CwdBaseDir {
+ type Target = PathBuf;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for CwdBaseDir {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl AsRef<Path> for CwdBaseDir {
+ fn as_ref(&self) -> &Path {
+ &self.0
+ }
+}
+
+
+
+impl<'de> Deserialize<'de> for CwdBaseDir {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where D: Deserializer<'de>
+ {
+ use serde::de::Error;
+ let path_buf = PathBuf::deserialize(deserializer)?;
+ Self::from_path(path_buf)
+ .map_err(|err| D::Error::custom(err))
+ }
+}
+
+impl Serialize for CwdBaseDir {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where S: Serializer,
+ {
+ use serde::ser::Error;
+ let path = self.to_base_path()
+ .map_err(|err| S::Error::custom(err))?;
+
+ path.serialize(serializer)
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn from_path_does_not_affect_absolute_paths() {
+ let path = Path::new("/the/dog");
+ let base_dir = CwdBaseDir::from_path(path).unwrap();
+ assert_eq!(&*base_dir, Path::new("/the/dog"))
+ }
+
+ #[test]
+ fn from_path_prefixes_with_cwd() {
+ let cwd = env::current_dir().unwrap();
+ let expected = cwd.join("./the/dog");
+
+ let base_dir = CwdBaseDir::from_path("./the/dog").unwrap();
+ assert_eq!(&*base_dir, &expected);
+ }
+
+ #[test]
+ fn to_base_path_removes_cwd_prefix() {
+ let cwd = env::current_dir().unwrap();
+ let dir = cwd.join("hy/there");
+ let base_dir = CwdBaseDir::new_unchanged(dir);
+ let path = base_dir.to_base_path().unwrap();
+ assert_eq!(path, Path::new("hy/there"));
+ }
+} \ No newline at end of file
diff --git a/template/src/handlebars.rs b/template/src/handlebars.rs
new file mode 100644
index 0000000..887dce1
--- /dev/null
+++ b/template/src/handlebars.rs
@@ -0,0 +1,96 @@
+use hbs;
+use serde::{Serialize, Deserialize};
+use galemu::{Bound, BoundExt, create_gal_wrapper_type, Ref};
+use failure::Error;
+
+
+use super::{
+ TemplateEngine,
+ TemplateEngineCanHandleData,
+ BodyTemplate,
+ PreparationData,
+ AdditionalCIds,
+ PathRebaseable,
+ UnsupportedPathError,
+ serde_impl
+};
+
+
+
+pub struct Handlebars {
+ inner: hbs::Handlebars,
+ name_counter: usize
+}
+
+impl Handlebars {
+
+ fn next_body_template_name(&mut self) -> String {
+ let name = format!("body_{}", self.name_counter);
+ self.name_counter += 1;
+ name
+ }
+}
+impl TemplateEngine for Handlebars {
+ type Id = String;
+
+ type LazyBodyTemplate = serde_impl::StandardLazyBodyTemplate;
+
+ fn load_body_template(&mut self, tmpl: Self::LazyBodyTemplate)
+ -> Result<BodyTemplate<Self>, Error>
+ {
+ let StandardLazyBodyTemplate {
+ path, embeddings, media_type
+ } = tmpl;
+
+ let name = self.next_body_template_name();
+ self.inner.register_template_file(name, path)?;
+
+ let media_type =
+ if let Some(media_type) = media_type {
+ media_type
+ } else if let Some(extension) = path.extension().and_then(|osstr| osstr.as_str()) {
+ match extension {
+ "html" => "text/html; charset=utf-8".parse().unwrap(),
+ "txt" => "text/plain; charset=utf-8".parse().unwrap()
+ }
+ } else {
+ return Err(failure::err_msg(
+ "handlebars requires html/txt file extension or media type given in template spec"
+ ));
+ };
+
+ Ok(BodyTemplate {
+ template_id: name,
+ media_type: TODO,
+ inline_embeddings: Default::default(),
+ })
+ }
+
+ fn load_subject_template(&mut self, template_string: String)
+ -> Result<Self::Id, Error>
+ {
+ Ok(self.inner.register_template_string("subject".to_owned(), template_string)?)
+ }
+}
+
+/// Additional trait a template engine needs to implement for the types it can process as input.
+///
+/// This could for example be implemented in a wild card impl for the template engine for
+/// any data `D` which implements `Serialize`.
+impl<D> TemplateEngineCanHandleData<D> for Handlebars
+ where D: Serialize
+{
+ fn render<'r, 'a>(
+ &'r self,
+ id: &'r Self::Id,
+ data: &'r D,
+ additional_cids: AdditionalCIds<'r>
+ ) -> Result<String, Error> {
+ Ok(self.inner.render(id, SerHelper { data, cids: additional_cid })?)
+ }
+}
+
+struct SerHelper<'r, D> {
+ data: &'r D,
+ cids: AdditionalCIds<'r>
+} \ No newline at end of file
diff --git a/template/src/lib.rs b/template/src/lib.rs
new file mode 100644
index 0000000..b889213
--- /dev/null
+++ b/template/src/lib.rs
@@ -0,0 +1,411 @@
+extern crate failure;
+extern crate serde;
+extern crate futures;
+extern crate mail_core;
+extern crate mail_headers;
+extern crate vec1;
+extern crate toml;
+extern crate maybe_owned;
+#[cfg(feature="handlebars")]
+extern crate handlebars as hbs;
+
+#[cfg(all(feature="handlebars", not(feature="handlebars-bindings")))]
+compile_error!("use feature `handlebars-bindings` instead of opt-dep-auto-feature `handlebars`");
+
+use std::{
+ fs,
+ collections::HashMap,
+ fmt::Debug,
+ path::{Path, PathBuf},
+ ops::Deref
+};
+
+use serde::{
+ Serialize,
+ Deserialize
+};
+use failure::{Fail, Error};
+use futures::{
+ Future, Poll, Async,
+ try_ready,
+ future::{self, Join, Either}
+};
+use vec1::Vec1;
+use maybe_owned::MaybeOwned;
+
+use mail_core::{
+ Resource,
+ Data, Metadata,
+ Context, ResourceContainerLoadingFuture,
+ compose::{MailParts, BodyPart},
+ Mail
+};
+use mail_headers::{
+ HeaderKind, Header,
+ header_components::MediaType,
+ headers
+};
+
+pub mod serde_impl;
+mod base_dir;
+mod path_rebase;
+mod additional_cid;
+
+// #[cfg(feature="handlebars")]
+// pub mod handlebars;
+
+pub use self::base_dir::*;
+pub use self::path_rebase::*;
+pub use self::additional_cid::*;
+
+/// Trait used to bind/implement template engines.
+pub trait TemplateEngine: Sized {
+ type Id: Debug;
+
+ type LazyBodyTemplate: PathRebaseable + Debug + Send + Serialize + for<'a> Deserialize<'a>;
+
+ fn load_body_template(&mut self, tmpl: Self::LazyBodyTemplate)
+ -> Result<BodyTemplate<Self>, Error>;
+
+ fn load_subject_template(&mut self, template_string: String)
+ -> Result<Self::Id, Error>;
+}
+
+/// Additional trait a template engine needs to implement for the types it can process as input.
+///
+/// This could for example be implemented in a wild card impl for the template engine for
+/// any data `D` which implements `Serialize`.
+pub trait TemplateEngineCanHandleData<D>: TemplateEngine {
+
+ fn render<'r, 'a>(
+ &'r self,
+ id: &'r Self::Id,
+ data: &'r D,
+ additional_cids: AdditionalCIds<'r>
+ ) -> Result<String, Error>;
+}
+
+/// Load a template as described in a toml file.
+pub fn load_toml_template_from_path<TE, C>(
+ engine: TE,
+ path: PathBuf,
+ ctx: &C
+) -> impl Future<Item=Template<TE>, Error=Error>
+ where TE: TemplateEngine + 'static, C: Context
+{
+
+ let ctx2 = ctx.clone();
+ ctx.offload_fn(move || {
+ let content = fs::read_to_string(&path)?;
+ let base: serde_impl::TemplateBase<TE> = toml::from_str(&content)?;
+ let base_dir = path.parent().unwrap_or_else(||Path::new("."));
+ let base_dir = CwdBaseDir::from_path(base_dir)?;
+ Ok((base, base_dir))
+ }).and_then(move |(base, base_dir)| base.load(engine, base_dir, &ctx2))