diff options
Diffstat (limited to 'template')
-rw-r--r-- | template/Cargo.toml | 39 | ||||
-rw-r--r-- | template/README.md | 126 | ||||
-rw-r--r-- | template/notes/notes.md | 331 | ||||
-rw-r--r-- | template/src/additional_cid.rs | 60 | ||||
-rw-r--r-- | template/src/base_dir.rs | 153 | ||||
-rw-r--r-- | template/src/handlebars.rs | 96 | ||||
-rw-r--r-- | template/src/lib.rs | 411 | ||||
-rw-r--r-- | template/src/path_rebase.rs | 191 | ||||
-rw-r--r-- | template/src/serde_impl.rs | 447 |
9 files changed, 1854 insertions, 0 deletions
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)) +} + +/// Load a template as described in a toml string; +pub fn load_toml_template_from_str<TE, C>( + engine: TE, + content: &str, + ctx: &C +) -> impl Future<Item=Template<TE>, Error=Error> + where TE: TemplateEngine, C: Context +{ + let base: serde_impl::TemplateBase<TE> = + match toml::from_str(content) { + Ok(base) => base, + Err(err) => { return Either::B(future::err(Error::from(err))); } + }; + + let base_dir = + match CwdBaseDir::from_path(Path::new(".")) { + Ok(base_dir) => base_dir, + Err(err) => { return Either::B(future::err(Error::from(err))) } + }; + + Either::A(base.load(engine, base_dir, ctx)) +} + + +/// A Mail template. +#[derive(Debug)] +pub struct Template<TE: TemplateEngine> { + template_name: String, + base_dir: CwdBaseDir, + subject: Subject<TE>, + /// This can only be in the loaded form _iff_ this is coupled + /// with a template engine instance, as using it with the wrong + /// template engine will lead to potential bugs and panics. + bodies: Vec1<BodyTemplate<TE>>, + //TODO: make sure + embeddings: HashMap<String, Resource>, + attachments: Vec<Resource>, + engine: TE, +} + +impl<TE> Template<TE> + where TE: TemplateEngine +{ + pub fn inline_embeddings(&self) -> &HashMap<String, Resource> |