From ce63ad36c703dad8f0639f9c53c9197419dec95c Mon Sep 17 00:00:00 2001 From: Philipp Korber Date: Thu, 22 Nov 2018 14:20:48 +0100 Subject: refactor: prepared for merging with another repo --- template/Cargo.toml | 39 ++++ template/README.md | 126 ++++++++++++ template/notes/notes.md | 331 ++++++++++++++++++++++++++++++ template/src/additional_cid.rs | 60 ++++++ template/src/base_dir.rs | 153 ++++++++++++++ template/src/handlebars.rs | 96 +++++++++ template/src/lib.rs | 411 +++++++++++++++++++++++++++++++++++++ template/src/path_rebase.rs | 191 ++++++++++++++++++ template/src/serde_impl.rs | 447 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1854 insertions(+) create mode 100644 template/Cargo.toml create mode 100644 template/README.md create mode 100644 template/notes/notes.md create mode 100644 template/src/additional_cid.rs create mode 100644 template/src/base_dir.rs create mode 100644 template/src/handlebars.rs create mode 100644 template/src/lib.rs create mode 100644 template/src/path_rebase.rs create mode 100644 template/src/serde_impl.rs (limited to 'template') 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 "] +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 }` + +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 `; 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. `' ... ) + (image/png (Content-ID ) ... ) + ... )) + (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 ... '' ... ) ) + + (image/png (Content-ID ) ... ) ) + + (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: +` *(";" "=" )` 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] +} + +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]) -> 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(&self, serializer: S) -> Result + 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

(path: P) -> Result + where P: AsRef + Into + { + 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 for CwdBaseDir { + fn as_ref(&self) -> &Path { + &self.0 + } +} + + + +impl<'de> Deserialize<'de> for CwdBaseDir { + fn deserialize(deserializer: D) -> Result + 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(&self, serializer: S) -> Result + 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, 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 + { + 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 TemplateEngineCanHandleData for Handlebars + where D: Serialize +{ + fn render<'r, 'a>( + &'r self, + id: &'r Self::Id, + data: &'r D, + additional_cids: AdditionalCIds<'r> + ) -> Result { + 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, Error>; + + fn load_subject_template(&mut self, template_string: String) + -> Result; +} + +/// 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: TemplateEngine { + + fn render<'r, 'a>( + &'r self, + id: &'r Self::Id, + data: &'r D, + additional_cids: AdditionalCIds<'r> + ) -> Result; +} + +/// Load a template as described in a toml file. +pub fn load_toml_template_from_path( + engine: TE, + path: PathBuf, + ctx: &C +) -> impl Future, 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 = 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( + engine: TE, + content: &str, + ctx: &C +) -> impl Future, Error=Error> + where TE: TemplateEngine, C: Context +{ + let base: serde_impl::TemplateBase = + 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 { + template_name: String, + base_dir: CwdBaseDir, + subject: Subject, + /// 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>, + //TODO: make sure + embeddings: HashMap, + attachments: Vec, + engine: TE, +} + +impl Template + where TE: TemplateEngine +{ + pub fn inline_embeddings(&self) -> &HashMap { + &self.embeddings + } + + pub fn attachments(&self) -> &[Resource] { + &self.attachments + } + + pub fn engine(&self) -> &TE { + &self.engine + } + + pub fn bodies(&self) -> &[BodyTemplate] { + &self.bodies + } + + pub fn subject_template_id(&self) -> &TE::Id { + &self.subject.template_id + } +} + + +/// Represents one of potentially many alternate bodies in a template. +#[derive(Debug)] +pub struct BodyTemplate { + pub template_id: TE::Id, + pub media_type: MediaType, + pub inline_embeddings: HashMap +} + +impl BodyTemplate + where TE: TemplateEngine +{ + pub fn template_id(&self) -> &TE::Id { + &self.template_id + } + + pub fn media_type(&self) -> &MediaType { + &self.media_type + } + + pub fn inline_embeddings(&self) -> &HashMap { + &self.inline_embeddings + } +} + +/// Represents a template used for generating the subject of a mail. +#[derive(Debug)] +pub struct Subject { + template_id: TE::Id +} + +impl Subject + where TE: TemplateEngine +{ + pub fn template_id(&self) -> &TE::Id { + &self.template_id + } +} + +/// Automatically provides the `prepare_to_render` method for all `Templates` +/// +/// This trait is implemented for all `Templates`/`D`(data) combinations where +/// the templates template engine can handle the given data (impl. `TemplateEngineCanHandleData`) +/// +/// This trait should not be implemented by hand. +pub trait TemplateExt + where TE: TemplateEngine + TemplateEngineCanHandleData +{ + + fn render_to_mail_parts<'r>(&self, data: LoadedTemplateData<'r, D>, ctx: &impl Context) + -> Result<(MailParts, Header), Error>; + + fn render<'r>(&self, data: LoadedTemplateData<'r, D>, ctx: &impl Context) -> Result { + let (parts, subject) = self.render_to_mail_parts(data, ctx)?; + let mut mail = parts.compose(); + mail.insert_header(subject); + Ok(mail) + } +} + + +impl TemplateExt for Template + where TE: TemplateEngine + TemplateEngineCanHandleData +{ + fn render_to_mail_parts<'r>(&self, data: LoadedTemplateData<'r, D>, ctx: &impl Context) + -> Result<(MailParts, Header), Error> + { + let TemplateData { + data, + inline_embeddings, + mut attachments + } = data.into(); + + let subject = self.engine().render( + self.subject_template_id(), + &data, + AdditionalCIds::new(&[]) + )?; + + let subject = headers::Subject::auto_body(subject)?; + + //TODO use Vec1 try_map instead of loop + let mut bodies = Vec::new(); + for body in self.bodies().iter() { + let raw = self.engine().render( + body.template_id(), + &data, + AdditionalCIds::new(&[ + &inline_embeddings, + body.inline_embeddings(), + self.inline_embeddings() + ]) + )?; + + let data = Data::new( + raw.into_bytes(), + Metadata { + file_meta: Default::default(), + media_type: body.media_type().clone(), + content_id: ctx.generate_content_id() + } + ); + + let inline_embeddings = body.inline_embeddings() + .values() + .cloned() + .collect(); + + bodies.push(BodyPart { + resource: Resource::Data(data), + inline_embeddings, + attachments: Vec::new() + }); + } + + attachments.extend(self.attachments().iter().cloned()); + + let mut inline_embeddings_vec = Vec::new(); + for (key, val) in self.inline_embeddings() { + if !inline_embeddings.contains_key(key) { + inline_embeddings_vec.push(val.clone()) + } + } + + inline_embeddings_vec.extend(inline_embeddings.into_iter().map(|(_,v)|v)); + + let parts = MailParts { + //UNWRAP_SAFE (complexly mapping a Vec1 is safe) + alternative_bodies: Vec1::from_vec(bodies).unwrap(), + inline_embeddings: inline_embeddings_vec, + attachments + }; + + Ok((parts, subject)) + } +} + +pub struct TemplateData<'a, D: 'a> { + pub data: MaybeOwned<'a, D>, + pub attachments: Vec, + pub inline_embeddings: HashMap +} + +impl<'a, D> TemplateData<'a, D> { + + pub fn load(self, ctx: &impl Context) -> DataLoadingFuture<'a, D> { + let TemplateData { + data, + attachments, + inline_embeddings + } = self; + + let loading_fut = Resource::load_container(inline_embeddings, ctx) + .join(Resource::load_container(attachments, ctx)); + + DataLoadingFuture { + payload: Some(data), + loading_fut + } + } +} +impl From for TemplateData<'static, D> { + fn from(data: D) -> Self { + TemplateData { + data: data.into(), + attachments: Default::default(), + inline_embeddings: Default::default() + } + } +} + +impl<'a, D> From<&'a D> for TemplateData<'a, D> { + fn from(data: &'a D) -> Self { + TemplateData { + data: data.into(), + attachments: Default::default(), + inline_embeddings: Default::default() + } + } +} + +pub struct LoadedTemplateData<'a, D: 'a>(TemplateData<'a, D>); + +impl<'a, D> From<&'a D> for LoadedTemplateData<'a, D> { + fn from(data: &'a D) -> Self { + LoadedTemplateData(TemplateData::from(data)) + } +} + +impl From for LoadedTemplateData<'static, D> { + fn from(data: D) -> Self { + LoadedTemplateData(TemplateData::from(data)) + } +} + +impl<'a, D> Deref for LoadedTemplateData<'a, D> { + type Target = TemplateData<'a, D>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a, D> Into> for LoadedTemplateData<'a, D> { + fn into(self) -> TemplateData<'a, D> { + let LoadedTemplateData(data) = self; + data + } +} + +/// Future returned when preparing a template for rendering. +pub struct DataLoadingFuture<'a, D: 'a> { + payload: Option>, + loading_fut: Join< + ResourceContainerLoadingFuture>, + ResourceContainerLoadingFuture> + > +} + +impl<'a, D> Future for DataLoadingFuture<'a, D> { + type Item = LoadedTemplateData<'a, D>; + type Error = Error; + + fn poll(&mut self) -> Poll { + let ( + inline_embeddings, + attachments + ) = try_ready!(self.loading_fut.poll()); + + //UNWRAP_SAFE only non if polled after resolved + let data = self.payload.take().unwrap(); + + let inner = TemplateData { + data, + inline_embeddings, + attachments + }; + + Ok(Async::Ready(LoadedTemplateData(inner))) + } +} diff --git a/template/src/path_rebase.rs b/template/src/path_rebase.rs new file mode 100644 index 0000000..9818aa3 --- /dev/null +++ b/template/src/path_rebase.rs @@ -0,0 +1,191 @@ +use std::{ + path::{Path, PathBuf}, + mem +}; + +use mail_core::{IRI, Resource}; +use failure::{Fail, Context}; + +#[derive(Fail, Debug)] +#[fail(display = "unsupported path, only paths with following constraint are allowed: {}", _0)] +pub struct UnsupportedPathError(Context<&'static str>); + +impl UnsupportedPathError { + pub fn new(violated_constraint: &'static str) -> Self { + UnsupportedPathError(Context::new(violated_constraint)) + } +} + +pub trait PathRebaseable { + /// Prefixes path in the type with `base_dir`. + /// + /// # Error + /// + /// Some implementors might not support all paths. + /// For example a implementor requiring rust string + /// compatible paths might return a + /// `Err(UnsupportedPathError::new("utf-8"))`. + fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError>; + + /// Removes the `base_dir` prefix. + /// + /// # Error + /// + /// Some implementors might not support all paths. + /// For example a implementor requiring rust string + /// compatible paths might return a + /// `Err(UnsupportedPathError::new("utf-8"))`. + fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError>; +} + +impl PathRebaseable for PathBuf { + fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError> + { + let new_path; + if self.is_relative() { + new_path = base_dir.as_ref().join(&self); + } else { + return Ok(()); + } + mem::replace(self, new_path); + Ok(()) + } + + fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError> + { + let new_path; + if let Ok(path) = self.strip_prefix(base_dir) { + new_path = path.to_owned(); + } else { + return Ok(()); + } + mem::replace(self, new_path); + Ok(()) + } +} + +impl PathRebaseable for IRI { + fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError> + { + if self.scheme() != "path" { + return Ok(()); + } + + let new_tail = { + let path = Path::new(self.tail()); + if path.is_relative() { + base_dir.as_ref().join(path) + } else { + return Ok(()); + } + }; + + let new_tail = new_tail.to_str() + .ok_or_else(|| UnsupportedPathError::new("utf-8"))?; + + let new_iri = self.with_tail(new_tail); + mem::replace(self, new_iri); + Ok(()) + } + + fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError> + { + if self.scheme() != "path" { + return Ok(()); + } + + let new_iri = { + let path = Path::new(self.tail()); + + if let Ok(path) = path.strip_prefix(base_dir) { + //UNWRAP_SAFE: we just striped some parts, this can + // not make it lose it's string-ness + let new_tail = path.to_str().unwrap(); + self.with_tail(new_tail) + } else { + return Ok(()); + } + }; + + mem::replace(self, new_iri); + Ok(()) + } +} + +impl PathRebaseable for Resource { + fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError> + { + if let &mut Resource::Source(ref mut source) = self { + source.iri.rebase_to_include_base_dir(base_dir)?; + } + Ok(()) + } + + fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError> + { + if let &mut Resource::Source(ref mut source) = self { + source.iri.rebase_to_exclude_base_dir(base_dir)?; + } + Ok(()) + } + +} + + + +#[cfg(test)] +mod test { + use super::*; + use mail_core::Source; + + #[test] + fn rebase_on_path() { + let mut path = Path::new("/prefix/suffix.yup").to_owned(); + path.rebase_to_exclude_base_dir("/prefix").unwrap(); + assert_eq!(path, Path::new("suffix.yup")); + path.rebase_to_include_base_dir("./nfix").unwrap(); + path.rebase_to_include_base_dir("/mfix").unwrap(); + assert_eq!(path, Path::new("/mfix/nfix/suffix.yup")); + path.rebase_to_exclude_base_dir("/wrong").unwrap(); + assert_eq!(path, Path::new("/mfix/nfix/suffix.yup")); + } + + #[test] + fn rebase_on_iri() { + let mut iri: IRI = "path:/prefix/suffix.yup".parse().unwrap(); + iri.rebase_to_exclude_base_dir("/prefix").unwrap(); + assert_eq!(iri.as_str(), "path:suffix.yup"); + iri.rebase_to_include_base_dir("nfix").unwrap(); + iri.rebase_to_include_base_dir("/mfix").unwrap(); + assert_eq!(iri.as_str(), "path:/mfix/nfix/suffix.yup"); + iri.rebase_to_exclude_base_dir("/wrong").unwrap(); + assert_eq!(iri.as_str(), "path:/mfix/nfix/suffix.yup"); + } + + #[test] + fn rebase_on_resource() { + let mut resource = Resource::Source(Source { + iri: "path:abc/def".parse().unwrap(), + use_media_type: Default::default(), + use_file_name: Default::default() + }); + + resource.rebase_to_include_base_dir("./abc").unwrap(); + resource.rebase_to_include_base_dir("/pre").unwrap(); + resource.rebase_to_exclude_base_dir("/pre").unwrap(); + resource.rebase_to_exclude_base_dir("abc").unwrap(); + resource.rebase_to_include_base_dir("abc").unwrap(); + + if let Resource::Source(Source { iri, ..}) = resource { + assert_eq!(iri.as_str(), "path:abc/abc/def"); + } else { unreachable!() } + } +} \ No newline at end of file diff --git a/template/src/serde_impl.rs b/template/src/serde_impl.rs new file mode 100644 index 0000000..2ab709d --- /dev/null +++ b/template/src/serde_impl.rs @@ -0,0 +1,447 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf} +}; + +use serde::{ + Serialize, Deserialize, + de::{ + Deserializer, + }, +}; +use failure::Error; +use futures::{Future, future::{self, Either}}; +use vec1::Vec1; + +use mail_core::{Resource, Source, IRI, Context}; +use mail_headers::header_components::MediaType; + +use super::{ + Template, + TemplateEngine, + CwdBaseDir, + PathRebaseable, + Subject, + UnsupportedPathError, +}; + +/// Type used when deserializing a template using serde. +/// +/// This type should only be used as intermediate type +/// used for deserialization as templates need to be +/// bundled with a template engine. +/// +/// # Serialize/Deserialize +/// +/// The derserialization currently only works with +/// self-describing data formats. +/// +/// There are a number of shortcuts to deserialize +/// resources (from emebddings and/or attachments): +/// +/// - Resources can be deserialized normally (from a externally tagged enum) +/// - Resources can be deserialized from the serialized repr of `Source` +/// - Resources can be deserialized from a string which is used to create +/// a `Resource::Source` with a iri using the `path` scheme and the string +/// content as the iris "tail". +#[derive(Debug, Serialize, Deserialize)] +pub struct TemplateBase { + #[serde(rename="name")] + template_name: String, + #[serde(default)] + base_dir: Option, + subject: LazySubject, + bodies: Vec1, + //TODO impl. deserialize where + // resource:String -> IRI::new("path", resource) -> Resource::Source + #[serde(deserialize_with="deserialize_embeddings")] + embeddings: HashMap, + #[serde(deserialize_with="deserialize_attachments")] + attachments: Vec, +} + +impl TemplateBase + where TE: TemplateEngine +{ + + //TODO!! make this load all embeddings/attachments and make it a future + /// Couples the template base with a specific engine instance. + pub fn load(self, mut engine: TE, default_base_dir: CwdBaseDir, ctx: &impl Context) -> impl Future, Error=Error> { + let TemplateBase { + template_name, + base_dir, + subject, + bodies, + mut embeddings, + mut attachments + } = self; + + let base_dir = base_dir.unwrap_or(default_base_dir); + + //FIXME[rust/catch block] use catch block + let catch_res = (|| -> Result<_, Error> { + let subject = Subject{ template_id: engine.load_subject_template(subject.template_string)? }; + + let bodies = bodies.try_mapped(|mut lazy_body| -> Result<_, Error> { + lazy_body.rebase_to_include_base_dir(&base_dir)?; + Ok(engine.load_body_template(lazy_body)?) + })?; + + for embedding in embeddings.values_mut() { + embedding.rebase_to_include_base_dir(&base_dir)?; + } + + for attachment in attachments.iter_mut() { + attachment.rebase_to_include_base_dir(&base_dir)?; + } + + Ok((subject, bodies)) + })(); + + let (subject, bodies) = + match catch_res { + Ok(vals) => vals, + Err(err) => { return Either::B(future::err(err)); } + }; + + let loading_fut = Resource::load_container(embeddings, ctx) + .join(Resource::load_container(attachments, ctx)); + + let fut = loading_fut + .map_err(Error::from) + .map(|(embeddings, attachments)| { + Template { + template_name, + base_dir, + subject, + bodies, + embeddings, + attachments, + engine + } + }); + + Either::A(fut) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct LazySubject { + #[serde(flatten)] + template_string: String +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum ResourceDeserializationHelper { + // This allows specifying resources in three ways. + // 1. as tagged enum `Resource` (e.g. `{"Source": { "iri": ...}}}`) + // 2. as struct `Source` (e.g. `{"iri": ...}` ) + // 3. as String which is interpreted as path iri + Normal(Resource), + FromSource(Source), + FromString(String) +} + +impl Into for ResourceDeserializationHelper { + fn into(self) -> Resource { + use self::ResourceDeserializationHelper::*; + match self { + Normal(resource) => resource, + FromString(string) => { + let source = Source { + //UNWRAP_SAFE: only scheme validation could fail, + // but its static "path" which is known to be valid + iri: IRI::from_parts("path", &string).unwrap(), + use_media_type: Default::default(), + use_file_name: Default::default() + }; + + Resource::Source(source) + }, + FromSource(source) => Resource::Source(source) + } + } +} + +pub fn deserialize_embeddings<'de, D>(deserializer: D) + -> Result, D::Error> + where D: Deserializer<'de> +{ + //FIXME[perf] write custom visitor etc. + let map = > + ::deserialize(deserializer)?; + + let map = map.into_iter() + .map(|(k, helper)| (k, helper.into())) + .collect(); + + Ok(map) +} + +pub fn deserialize_attachments<'de, D>(deserializer: D) + -> Result, D::Error> + where D: Deserializer<'de> +{ + //FIXME[perf] write custom visitor etc. + let vec = > + ::deserialize(deserializer)?; + + let vec = vec.into_iter() + .map(|helper| helper.into()) + .collect(); + + Ok(vec) +} + +//TODO make base dir default to the dir the template file is in if it's parsed from a template file. + +/// Common implementation for a type for [`TemplateEngine::LazyBodyTemplate`]. +/// +/// This impl. gives bodies a field `embeddings` which is a mapping of embedding +/// names to embeddings (using `deserialize_embeddings`) a `path` field which +/// allows specifying the template file (e.g. `"body.html"`) and can be relative +/// to the base dir. +#[derive(Debug, Serialize)] +pub struct StandardLazyBodyTemplate { + pub path: PathBuf, + pub embeddings: HashMap, + pub media_type: Option +} + + +impl PathRebaseable for StandardLazyBodyTemplate { + fn rebase_to_include_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError> + { + self.path.rebase_to_include_base_dir(base_dir) + } + + fn rebase_to_exclude_base_dir(&mut self, base_dir: impl AsRef) + -> Result<(), UnsupportedPathError> + { + self.path.rebase_to_exclude_base_dir(base_dir) + } +} + + +#[derive(Deserialize)] +#[serde(untagged)] +enum StandardLazyBodyTemplateDeserializationHelper { + ShortForm(String), + LongForm { + path: PathBuf, + #[serde(default)] + #[serde(deserialize_with="deserialize_embeddings")] + embeddings: HashMap, + #[serde(default)] + media_type: Option + } +} + +impl<'de> Deserialize<'de> for StandardLazyBodyTemplate { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> + { + use self::StandardLazyBodyTemplateDeserializationHelper::*; + let helper = StandardLazyBodyTemplateDeserializationHelper::deserialize(deserializer)?; + let ok_val = + match helper { + ShortForm(string) => { + StandardLazyBodyTemplate { + path: string.into(), + embeddings: Default::default(), + media_type: Default::default() + } + }, + LongForm {path, embeddings, media_type} => + StandardLazyBodyTemplate { path, embeddings, media_type } + }; + Ok(ok_val) + } +} + + +#[cfg(test)] +mod test { + use toml; + use super::*; + + fn test_source_iri(resource: &Resource, iri: &str) { + if let &Resource::Source(ref source) = resource { + assert_eq!(source.iri.as_str(), iri); + } else { + panic!("unexpected resource expected resource with source and iri {:?} but got {:?}", iri, resource); + } + } + + mod attachment_deserialization { + use super::*; + use super::super::deserialize_attachments; + + #[derive(Serialize, Deserialize)] + struct Wrapper { + #[serde(deserialize_with="deserialize_attachments")] + attachments: Vec + } + + + #[test] + fn should_deserialize_from_strings() { + let raw_toml = r#" + attachments = ["notes.md", "pic.xd"] + "#; + + let Wrapper { attachments } = toml::from_str(raw_toml).unwrap(); + + assert_eq!(attachments.len(), 2); + test_source_iri(&attachments[0], "path:notes.md"); + test_source_iri(&attachments[1], "path:pic.xd"); + } + + #[test] + fn should_deserialize_from_sources() { + let raw_toml = r#" + [[attachments]] + Source = {iri="https://fun.example"} + [[attachments]] + iri="path:pic.xd" + "#; + + let Wrapper { attachments } = toml::from_str(raw_toml).unwrap(); + + assert_eq!(attachments.len(), 2); + test_source_iri(&attachments[0], "https://fun.example"); + test_source_iri(&attachments[1], "path:pic.xd"); + } + + #[test] + fn check_if_data_is_deserializable_like_expected() { + use mail_core::Data; + + let raw_toml = r#" + media_type = "text/plain; charset=utf-8" + buffer = [65,65,65,66,65] + content_id = "c0rc3rcr0q0v32@example.example" + "#; + + let data: Data = toml::from_str(raw_toml).unwrap(); + + assert_eq!(data.content_id().as_str(), "c0rc3rcr0q0v32@example.example"); + assert_eq!(&**data.buffer(), b"AAABA" as &[u8]); + } + + #[test] + fn should_deserialize_from_data() { + let raw_toml = r#" + [[attachments]] + [attachments.Data] + media_type = "text/plain; charset=utf-8" + buffer = [65,65,65,66,65] + content_id = "c0rc3rcr0q0v32@example.example" + "#; + + let Wrapper { attachments } = toml::from_str(raw_toml).unwrap(); + + assert_eq!(attachments.len(), 1); + } + } + + mod embedding_deserialization { + use super::*; + use super::super::deserialize_embeddings; + + #[derive(Serialize, Deserialize)] + struct Wrapper { + #[serde(deserialize_with="deserialize_embeddings")] + embeddings: HashMap + } + + #[test] + fn should_deserialize_with_short_forms() { + let raw_toml = r#" + [embeddings] + pic = "hy-ya" + pic2 = { iri = "path:ay-ya" } + [embeddings.pic3.Data] + media_type = "text/plain; charset=utf-8" + buffer = [65,65,65,66,65] + content_id = "c0rc3rcr0q0v32@example.example" + [embeddings.pic4.Source] + iri = "path:nay-nay-way" + "#; + + let Wrapper { embeddings } = toml::from_str(raw_toml).unwrap(); + + assert_eq!(embeddings.len(), 4); + assert!(embeddings.contains_key("pic")); + assert!(embeddings.contains_key("pic2")); + assert!(embeddings.contains_key("pic3")); + assert!(embeddings.contains_key("pic4")); + test_source_iri(&embeddings["pic"], "path:hy-ya"); + test_source_iri(&embeddings["pic2"], "path:ay-ya"); + test_source_iri(&embeddings["pic4"], "path:nay-nay-way"); + assert_eq!(embeddings["pic3"].content_id().unwrap().as_str(), "c0rc3rcr0q0v32@example.example"); + } + } + + #[allow(non_snake_case)] + mod StandardLazyBodyTemplate { + use super::*; + use super::super::StandardLazyBodyTemplate; + + #[derive(Serialize, Deserialize)] + struct Wrapper { + body: StandardLazyBodyTemplate + } + + #[test] + fn should_deserialize_from_string() { + let toml_str = r#" + body = "template.html.hbs" + "#; + + let Wrapper { body } = toml::from_str(toml_str).unwrap(); + assert_eq!(body.path.to_str().unwrap(), "template.html.hbs"); + assert_eq!(body.embeddings.len(), 0); + } + + #[test] + fn should_deserialize_from_object_without_embeddings() { + let toml_str = r#" + body = { path="t.d" } + "#; + + let Wrapper { body }= toml::from_str(toml_str).unwrap(); + assert_eq!(body.path.to_str().unwrap(), "t.d"); + assert_eq!(body.embeddings.len(), 0); + } + + #[test] + fn should_deserialize_from_object_with_empty_embeddings() { + let toml_str = r#" + body = { path="t.d", embeddings={} } + "#; + + let Wrapper { body } = toml::from_str(toml_str).unwrap(); + assert_eq!(body.path.to_str().unwrap(), "t.d"); + assert_eq!(body.embeddings.len(), 0); + } + + #[test] + fn should_deserialize_from_object_with_short_from_embeddings() { + let toml_str = r#" + body = { path="t.d", embeddings={ pic1="the_embeddings" } } + "#; + + let Wrapper { body } = toml::from_str(toml_str).unwrap(); + assert_eq!(body.path.to_str().unwrap(), "t.d"); + assert_eq!(body.embeddings.len(), 1); + + let (key, resource) = body.embeddings.iter().next().unwrap(); + assert_eq!(key, "pic1"); + + test_source_iri(resource, "path:the_embeddings"); + } + } +} \ No newline at end of file -- cgit v1.2.3