1pub mod loader;
2pub mod parser;
3pub mod render;
4
5use std::sync::Arc;
6
7use lettre::message::header::ContentType;
8use lettre::message::Body;
9
10#[derive(Clone, Debug, Default, serde::Deserialize)]
11pub struct Config {
12 #[serde(default)]
13 pub loader: loader::Config,
14 #[serde(default)]
15 pub parser: parser::Config,
16 #[serde(default)]
17 pub render: render::Config,
18}
19
20#[derive(Debug, thiserror::Error)]
21pub enum Error {
22 #[error("unable to interpolate template with provided variables: {0}")]
23 Interpolation(#[from] handlebars::RenderError),
24 #[error("unable to load tempalte from provider: {0}")]
25 Loading(#[from] loader::Error),
26 #[error("unable to parse template: {0}")]
27 Parsing(#[from] mrml::prelude::parser::Error),
28 #[error("unable to render template: {0}")]
29 Rendering(#[from] mrml::prelude::render::Error),
30 #[error("unable to build email: {0}")]
31 Building(#[from] lettre::error::Error),
32}
33
34#[derive(Clone, Debug)]
35pub struct Engine {
36 loader: Arc<loader::Loader>,
37 parser: Arc<mrml::prelude::parser::AsyncParserOptions>,
38 render: Arc<mrml::prelude::render::RenderOptions>,
39}
40
41impl From<Config> for Engine {
42 fn from(value: Config) -> Self {
43 Self {
44 loader: Arc::new(value.loader.into()),
45 parser: Arc::new(value.parser.into()),
46 render: Arc::new(value.render.into()),
47 }
48 }
49}
50
51impl Engine {
52 async fn load(&self, name: &str) -> Result<loader::prelude::Template, Error> {
53 self.loader.find_by_name(name).await.map_err(Error::Loading)
54 }
55
56 fn interpolate<T>(&self, input: &str, params: &T) -> Result<String, Error>
57 where
58 T: serde::Serialize,
59 {
60 let handlebar = handlebars::Handlebars::new();
61 Ok(handlebar.render_template(input, params)?)
62 }
63
64 async fn parse(&self, input: String) -> Result<mrml::mjml::Mjml, Error> {
65 mrml::async_parse_with_options(input, self.parser.clone())
66 .await
67 .map_err(Error::from)
68 }
69
70 fn render(&self, input: mrml::mjml::Mjml) -> Result<String, Error> {
71 Ok(input.render(&self.render)?)
72 }
73
74 pub async fn handle(&self, req: Request) -> Result<lettre::Message, Error> {
75 let template = self.load(req.name.as_str()).await?;
76 let res = self.interpolate(&template.content, &req.params)?;
80 let res = self.parse(res).await?;
81
82 let title = res.get_title();
83 let preview = res.get_preview();
84 let body = self.render(res)?;
85
86 let msg = lettre::Message::builder().from(req.from);
87 let msg = req.to.into_iter().fold(msg, |msg, item| msg.to(item));
88 let msg = req.cc.into_iter().fold(msg, |msg, item| msg.cc(item));
89 let msg = req.bcc.into_iter().fold(msg, |msg, item| msg.bcc(item));
90
91 let multipart = match preview {
92 Some(preview) => lettre::message::MultiPart::alternative_plain_html(preview, body),
93 None => lettre::message::MultiPart::alternative()
94 .singlepart(lettre::message::SinglePart::html(body)),
95 };
96
97 let multipart = req.attachments.into_iter().fold(multipart, |res, file| {
98 res.singlepart(
99 lettre::message::Attachment::new(file.filename)
100 .body(file.content, file.content_type),
101 )
102 });
103
104 let msg = msg
105 .subject(title.unwrap_or_default())
106 .multipart(multipart)?;
107
108 Ok(msg)
109 }
110}
111
112#[derive(Debug)]
113pub struct Attachment {
114 pub filename: String,
115 pub content_type: ContentType,
116 pub content: Body,
117}
118
119#[derive(Debug)]
120pub struct Request {
121 pub name: String,
122 pub from: lettre::message::Mailbox,
123 pub to: Vec<lettre::message::Mailbox>,
124 pub cc: Vec<lettre::message::Mailbox>,
126 pub bcc: Vec<lettre::message::Mailbox>,
128 pub params: serde_json::Value,
129 pub attachments: Vec<Attachment>,
130}
131
132pub struct Message {
133 pub title: Option<String>,
134 pub preview: Option<String>,
135 pub body: String,
136}