catapulte_engine/
lib.rs

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        // TODO handle embedded attachments
77        // TODO validate params with jsonapi from template
78
79        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    // carbon copy
125    pub cc: Vec<lettre::message::Mailbox>,
126    // blind carbon copy
127    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}