1mod builder;
49mod email;
50pub mod themes;
51
52pub use builder::EmailBuilder;
53pub use email::{Action, Email, GoToAction, Greeting, Table, TableColumns};
54use serde::{Deserialize, Serialize};
55use themes::{TemplateContext, Theme};
56
57pub struct Mailgen<T: Theme> {
58 theme: T,
59 branding: Branding,
60}
61
62impl<T: Theme> Mailgen<T> {
63 pub fn new(theme: T, branding: Branding) -> Self {
64 Self { theme, branding }
65 }
66
67 pub fn render_html(&self, email: &Email) -> Result<String, T::Error> {
68 let context = TemplateContext {
69 email,
70 branding: &self.branding,
71 };
72
73 self.theme.html(&context)
74 }
75
76 pub fn render_text(&self, email: &Email) -> Result<String, T::Error> {
77 let context = TemplateContext {
78 email,
79 branding: &self.branding,
80 };
81
82 self.theme.text(&context)
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct Branding {
90 pub name: String,
91 pub link: String,
92 pub logo: Option<String>,
93 pub copyright: Option<String>,
94 pub trouble_text: String,
95}
96
97impl Branding {
98 pub fn new<S: Into<String>>(name: S, link: S) -> Self {
99 let name = name.into();
100 let link = link.into();
101 let copyright = format!("Copyright © {name}. All rights reserved.");
102 let trouble_text = "If you're having trouble with the button '{ACTION}', copy and paste the URL below into your web browser."
103 .to_string();
104
105 Self {
106 name,
107 link,
108 trouble_text,
109 copyright: Some(copyright),
110 logo: None,
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use crate::builder::EmailBuilder;
118 use crate::{Action, Branding, Greeting, Mailgen, Table, TableColumns};
119
120 #[test]
121 #[cfg(feature = "default-theme")]
122 fn test_default_theme() -> Result<(), Box<dyn std::error::Error>> {
123 use crate::themes::DefaultTheme;
124
125 let theme = DefaultTheme::new()?;
126 let product = Branding::new("test product", "https://testproduct.com");
127 let mailgen = Mailgen::new(theme, product);
128
129 let email = EmailBuilder::new()
130 .summary("this is a test email that contains stuff to test...")
131 .greeting(Greeting::Name("person name"))
132 .intro("test intro")
133 .intro("another intro")
134 .dictionary("test key", "test value")
135 .dictionary("test key 2", "test value 2")
136 .action(Action {
137 text: "Test Action",
138 link: "https://test.com/action",
139 color: Some(("black", "white")),
140 ..Default::default()
141 })
142 .action(Action {
143 text: "Test Action 2",
144 link: "https://test.com/action2",
145 instructions: Some("test instruction"),
146 ..Default::default()
147 })
148 .outro("test outro 1")
149 .outro("test outro 2")
150 .signature("test signature...")
151 .build();
152
153 let rendered = mailgen.render_text(&email)?;
154 std::fs::write("./email.txt", rendered)?;
155
156 let rendered = mailgen.render_html(&email)?;
157 std::fs::write("./email.html", rendered)?;
158
159 Ok(())
160 }
161
162 #[test]
163 #[cfg(feature = "default-theme")]
164 fn test_tables() -> Result<(), Box<dyn std::error::Error>> {
165 use crate::themes::DefaultTheme;
166 use std::collections::HashMap;
167
168 let theme = DefaultTheme::new()?;
169 let branding = Branding::new("test product", "https://testproduct.com");
170 let mailgen = Mailgen::new(theme, branding);
171
172 let mut row1 = HashMap::new();
174 row1.insert("Item", "Product 1");
175 row1.insert("Price", "$10.99");
176 row1.insert("Quantity", "1");
177
178 let mut row2 = HashMap::new();
179 row2.insert("Item", "Product 2");
180 row2.insert("Price", "$24.99");
181 row2.insert("Quantity", "2");
182
183 let mut custom_alignment = HashMap::new();
184 custom_alignment.insert("Price", "right");
185 custom_alignment.insert("Quantity", "center");
186
187 let columns = TableColumns {
188 custom_width: None,
189 custom_alignment: Some(custom_alignment),
190 };
191
192 let table = Table {
193 title: "Order Summary",
194 data: vec![row1, row2],
195 columns: Some(columns),
196 };
197
198 let email = EmailBuilder::new()
199 .summary("Order Confirmation")
200 .greeting(Greeting::Name("Customer"))
201 .intro("Thank you for your order!")
202 .table(table)
203 .outro("Your order will be processed soon.")
204 .signature("The Sales Team")
205 .build();
206
207 let rendered = mailgen.render_html(&email)?;
208 std::fs::write("./email_with_table.html", rendered)?;
209
210 let rendered = mailgen.render_text(&email)?;
211 std::fs::write("./email_with_table.txt", rendered)?;
212
213 Ok(())
214 }
215
216 #[test]
217 #[cfg(feature = "default-theme")]
218 fn create_eml_file() -> Result<(), Box<dyn std::error::Error>> {
219 use crate::themes::DefaultTheme;
220 use std::collections::HashMap;
221 use std::fs::File;
222 use std::io::Write;
223
224 let theme = DefaultTheme::new()?;
225 let product = Branding::new("test product", "https://testproduct.com");
226 let mailgen = Mailgen::new(theme, product);
227
228 let mut row1 = HashMap::new();
230 row1.insert("Item", "Product 1");
231 row1.insert("Price", "$10.99");
232 row1.insert("Quantity", "1");
233
234 let mut row2 = HashMap::new();
235 row2.insert("Item", "Product 2");
236 row2.insert("Price", "$24.99");
237 row2.insert("Quantity", "2");
238
239 let mut custom_alignment = HashMap::new();
240 custom_alignment.insert("Price", "right");
241 custom_alignment.insert("Quantity", "center");
242
243 let columns = TableColumns {
244 custom_width: None,
245 custom_alignment: Some(custom_alignment),
246 };
247
248 let table = Table {
249 title: "Order Summary",
250 data: vec![row1, row2],
251 columns: Some(columns),
252 };
253
254 let email = EmailBuilder::new()
255 .summary("Email Test Subject")
256 .greeting(Greeting::Name("Test User"))
257 .intro("Welcome to our service!")
258 .intro("We're excited to have you on board.")
259 .dictionary("Account", "test@example.com")
260 .dictionary("Plan", "Premium")
261 .action(Action {
262 text: "Confirm Account",
263 link: "https://example.com/confirm",
264 color: Some(("#48cfad", "#ffffff")),
265 ..Default::default()
266 })
267 .table(table)
268 .outro("Need help, or have questions?")
269 .outro("Just reply to this email, we'd love to help.")
270 .signature("The Example Team")
271 .build();
272
273 let text_content = mailgen.render_text(&email)?;
274 let html_content = mailgen.render_html(&email)?;
275
276 let boundary = "------------MAILGEN_BOUNDARY";
278
279 let mut eml_file = File::create("./email_test.eml")?;
281
282 writeln!(
284 eml_file,
285 "From: \"Test Product\" <no-reply@testproduct.com>"
286 )?;
287 writeln!(eml_file, "To: \"Test User\" <test@example.com>")?;
288 writeln!(eml_file, "Subject: Email Test Subject")?;
289 writeln!(eml_file, "MIME-Version: 1.0")?;
290 writeln!(
291 eml_file,
292 "Content-Type: multipart/alternative; boundary=\"{boundary}\""
293 )?;
294 writeln!(eml_file)?;
295
296 writeln!(eml_file, "--{boundary}")?;
298 writeln!(eml_file, "Content-Type: text/plain; charset=UTF-8")?;
299 writeln!(eml_file, "Content-Transfer-Encoding: 8bit")?;
300 writeln!(eml_file)?;
301 writeln!(eml_file, "{}", text_content)?;
302 writeln!(eml_file)?;
303
304 writeln!(eml_file, "--{boundary}")?;
306 writeln!(eml_file, "Content-Type: text/html; charset=UTF-8")?;
307 writeln!(eml_file, "Content-Transfer-Encoding: 8bit")?;
308 writeln!(eml_file)?;
309 writeln!(eml_file, "{}", html_content)?;
310 writeln!(eml_file)?;
311
312 writeln!(eml_file, "--{boundary}--")?;
314
315 Ok(())
316 }
317
318 #[test]
319 #[cfg(feature = "default-theme")]
320 fn test_go_to_action() -> Result<(), Box<dyn std::error::Error>> {
321 use crate::themes::DefaultTheme;
322
323 let theme = DefaultTheme::new()?;
324 let branding = Branding::new("Test Company", "https://example.com");
325 let mailgen = Mailgen::new(theme, branding);
326
327 let email = EmailBuilder::new()
328 .summary("Account Activation")
329 .greeting(Greeting::Name("John Doe"))
330 .intro("Welcome to our service! Please activate your account.")
331 .action(Action {
332 text: "Activate Account",
333 link: "https://example.com/activate",
334 instructions: Some("Click the button below to activate your account:"),
335 ..Default::default()
336 })
337 .go_to_action(
338 "Activate Now",
339 "https://example.com/activate",
340 "Activate your account with one click",
341 )
342 .outro("Need help? Just reply to this email.")
343 .build();
344
345 let rendered_html = mailgen.render_html(&email)?;
346
347 assert!(rendered_html.contains("application/ld+json"));
349 assert!(rendered_html.contains("Activate your account with one click"));
350
351 std::fs::write("./email_with_goto_action.html", rendered_html)?;
352
353 let rendered_text = mailgen.render_text(&email)?;
354 std::fs::write("./email_with_goto_action.txt", rendered_text)?;
355
356 Ok(())
357 }
358}