missive/
email.rs

1//! Email struct with builder pattern.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::address::{Address, ToAddress};
7use crate::attachment::Attachment;
8
9/// An email message.
10///
11/// Use the builder pattern to construct emails:
12///
13/// ```
14/// use missive::Email;
15///
16/// let email = Email::new()
17///     .from("sender@example.com")
18///     .to("recipient@example.com")
19///     .subject("Hello!")
20///     .text_body("Plain text content")
21///     .html_body("<h1>HTML content</h1>");
22/// ```
23///
24/// ## Fields
25///
26/// - `from`, `to`, `cc`, `bcc` - Addresses
27/// - `reply_to` - Reply-to addresses (supports multiple)
28/// - `subject`, `text_body`, `html_body` - Content
29/// - `attachments` - File attachments
30/// - `headers` - Custom email headers
31/// - `assigns` - Template variables (for use with templating systems)
32/// - `private` - Private storage for libraries/frameworks
33/// - `provider_options` - Provider-specific options (tags, templates, etc.)
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
35pub struct Email {
36    /// Sender address
37    pub from: Option<Address>,
38    /// Primary recipients
39    pub to: Vec<Address>,
40    /// Carbon copy recipients
41    pub cc: Vec<Address>,
42    /// Blind carbon copy recipients
43    pub bcc: Vec<Address>,
44    /// Reply-to addresses (supports multiple)
45    pub reply_to: Vec<Address>,
46    /// Email subject line
47    pub subject: String,
48    /// Plain text body
49    pub text_body: Option<String>,
50    /// HTML body
51    pub html_body: Option<String>,
52    /// File attachments
53    pub attachments: Vec<Attachment>,
54    /// Custom email headers
55    pub headers: HashMap<String, String>,
56    /// Template variables for use with templating systems.
57    pub assigns: HashMap<String, serde_json::Value>,
58    /// Private storage for libraries/frameworks (e.g., template paths, metadata).
59    pub private: HashMap<String, serde_json::Value>,
60    /// Provider-specific options (e.g., tracking, tags, templates)
61    pub provider_options: HashMap<String, serde_json::Value>,
62}
63
64impl Email {
65    /// Create a new empty email.
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Set the sender address.
71    ///
72    /// Accepts anything that implements `ToAddress`:
73    /// - `"email@example.com"` - just email
74    /// - `("Name", "email@example.com")` - name and email
75    /// - Custom types that implement `ToAddress`
76    pub fn from(mut self, addr: impl ToAddress) -> Self {
77        self.from = Some(addr.to_address());
78        self
79    }
80
81    /// Add a recipient.
82    ///
83    /// Can be called multiple times to add multiple recipients.
84    /// Accepts anything that implements `ToAddress`.
85    pub fn to(mut self, addr: impl ToAddress) -> Self {
86        self.to.push(addr.to_address());
87        self
88    }
89
90    /// Replace all recipients.
91    pub fn put_to(mut self, addrs: Vec<Address>) -> Self {
92        self.to = addrs;
93        self
94    }
95
96    /// Add a CC recipient.
97    /// Accepts anything that implements `ToAddress`.
98    pub fn cc(mut self, addr: impl ToAddress) -> Self {
99        self.cc.push(addr.to_address());
100        self
101    }
102
103    /// Replace all CC recipients.
104    pub fn put_cc(mut self, addrs: Vec<Address>) -> Self {
105        self.cc = addrs;
106        self
107    }
108
109    /// Add a BCC recipient.
110    /// Accepts anything that implements `ToAddress`.
111    pub fn bcc(mut self, addr: impl ToAddress) -> Self {
112        self.bcc.push(addr.to_address());
113        self
114    }
115
116    /// Replace all BCC recipients.
117    pub fn put_bcc(mut self, addrs: Vec<Address>) -> Self {
118        self.bcc = addrs;
119        self
120    }
121
122    /// Add a reply-to address.
123    ///
124    /// Can be called multiple times to add multiple reply-to addresses.
125    /// Accepts anything that implements `ToAddress`.
126    pub fn reply_to(mut self, addr: impl ToAddress) -> Self {
127        self.reply_to.push(addr.to_address());
128        self
129    }
130
131    /// Replace all reply-to addresses.
132    pub fn put_reply_to(mut self, addrs: Vec<Address>) -> Self {
133        self.reply_to = addrs;
134        self
135    }
136
137    /// Set the subject line.
138    pub fn subject(mut self, subject: impl Into<String>) -> Self {
139        self.subject = subject.into();
140        self
141    }
142
143    /// Set the plain text body.
144    pub fn text_body(mut self, body: impl Into<String>) -> Self {
145        self.text_body = Some(body.into());
146        self
147    }
148
149    /// Set the HTML body.
150    pub fn html_body(mut self, body: impl Into<String>) -> Self {
151        self.html_body = Some(body.into());
152        self
153    }
154
155    /// Add an attachment.
156    pub fn attachment(mut self, attachment: Attachment) -> Self {
157        self.attachments.push(attachment);
158        self
159    }
160
161    /// Add a custom header.
162    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
163        self.headers.insert(name.into(), value.into());
164        self
165    }
166
167    /// Set a provider-specific option.
168    ///
169    /// These are passed to the adapter for provider-specific features
170    /// (e.g., SendGrid categories, Postmark tags, Resend templates).
171    ///
172    /// # Example
173    ///
174    /// ```rust,ignore
175    /// Email::new()
176    ///     .provider_option("template_id", "welcome-email")
177    ///     .provider_option("tags", vec!["signup", "welcome"])
178    /// ```
179    pub fn provider_option(
180        mut self,
181        key: impl Into<String>,
182        value: impl Into<serde_json::Value>,
183    ) -> Self {
184        self.provider_options.insert(key.into(), value.into());
185        self
186    }
187
188    /// Store a template variable for use with templating systems.
189    ///
190    /// # Example
191    ///
192    /// ```rust,ignore
193    /// Email::new()
194    ///     .assign("username", "alice")
195    ///     .assign("action_url", "https://example.com/verify")
196    /// ```
197    pub fn assign(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
198        self.assigns.insert(key.into(), value.into());
199        self
200    }
201
202    /// Store a private value for frameworks/libraries.
203    ///
204    /// Reserved for framework use (e.g., template paths, metadata).
205    ///
206    /// # Example
207    ///
208    /// ```rust,ignore
209    /// Email::new()
210    ///     .put_private("template_path", "emails/welcome.html")
211    ///     .put_private("sent_at", chrono::Utc::now().to_rfc3339())
212    /// ```
213    pub fn put_private(
214        mut self,
215        key: impl Into<String>,
216        value: impl Into<serde_json::Value>,
217    ) -> Self {
218        self.private.insert(key.into(), value.into());
219        self
220    }
221
222    /// Check if the email has all required fields for sending.
223    pub fn is_valid(&self) -> bool {
224        self.from.is_some() && !self.to.is_empty()
225    }
226
227    /// Get all recipients (to + cc + bcc).
228    pub fn all_recipients(&self) -> Vec<&Address> {
229        self.to
230            .iter()
231            .chain(self.cc.iter())
232            .chain(self.bcc.iter())
233            .collect()
234    }
235
236    /// Check if the email has any attachments.
237    pub fn has_attachments(&self) -> bool {
238        !self.attachments.is_empty()
239    }
240
241    /// Get inline attachments only.
242    pub fn inline_attachments(&self) -> Vec<&Attachment> {
243        self.attachments.iter().filter(|a| a.is_inline()).collect()
244    }
245
246    /// Get regular (non-inline) attachments only.
247    pub fn regular_attachments(&self) -> Vec<&Attachment> {
248        self.attachments.iter().filter(|a| !a.is_inline()).collect()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_builder() {
258        let email = Email::new()
259            .from("sender@example.com")
260            .to("recipient@example.com")
261            .subject("Test")
262            .text_body("Hello");
263
264        assert_eq!(email.from.unwrap().email, "sender@example.com");
265        assert_eq!(email.to.len(), 1);
266        assert_eq!(email.to[0].email, "recipient@example.com");
267        assert_eq!(email.subject, "Test");
268        assert_eq!(email.text_body, Some("Hello".to_string()));
269    }
270
271    #[test]
272    fn test_multiple_recipients() {
273        let email = Email::new()
274            .to("one@example.com")
275            .to("two@example.com")
276            .cc("cc@example.com")
277            .bcc("bcc@example.com");
278
279        assert_eq!(email.to.len(), 2);
280        assert_eq!(email.cc.len(), 1);
281        assert_eq!(email.bcc.len(), 1);
282        assert_eq!(email.all_recipients().len(), 4);
283    }
284
285    #[test]
286    fn test_with_name() {
287        let email = Email::new().from(("Alice", "alice@example.com"));
288
289        let from = email.from.unwrap();
290        assert_eq!(from.email, "alice@example.com");
291        assert_eq!(from.name, Some("Alice".to_string()));
292    }
293
294    #[test]
295    fn test_is_valid() {
296        let invalid = Email::new().to("recipient@example.com");
297        assert!(!invalid.is_valid());
298
299        let valid = Email::new()
300            .from("sender@example.com")
301            .to("recipient@example.com");
302        assert!(valid.is_valid());
303    }
304
305    #[test]
306    fn test_headers() {
307        let email = Email::new()
308            .header("X-Custom", "value")
309            .header("X-Priority", "1");
310
311        assert_eq!(email.headers.get("X-Custom"), Some(&"value".to_string()));
312        assert_eq!(email.headers.get("X-Priority"), Some(&"1".to_string()));
313    }
314
315    #[test]
316    fn test_provider_options() {
317        let email = Email::new().provider_option("template_id", "welcome-email");
318
319        assert_eq!(
320            email.provider_options.get("template_id"),
321            Some(&serde_json::json!("welcome-email"))
322        );
323    }
324
325    #[test]
326    fn test_to_address_trait() {
327        struct User {
328            name: String,
329            email: String,
330        }
331
332        impl ToAddress for User {
333            fn to_address(&self) -> Address {
334                Address::with_name(&self.name, &self.email)
335            }
336        }
337
338        let user = User {
339            name: "Alice".to_string(),
340            email: "alice@example.com".to_string(),
341        };
342
343        let email = Email::new().to(&user);
344        assert_eq!(email.to[0].email, "alice@example.com");
345        assert_eq!(email.to[0].name, Some("Alice".to_string()));
346    }
347
348    #[test]
349    fn test_to_address_trait_all_methods() {
350        struct Contact {
351            name: String,
352            email: String,
353        }
354
355        impl ToAddress for Contact {
356            fn to_address(&self) -> Address {
357                Address::with_name(&self.name, &self.email)
358            }
359        }
360
361        let sender = Contact {
362            name: "Sender".to_string(),
363            email: "sender@example.com".to_string(),
364        };
365        let recipient = Contact {
366            name: "Recipient".to_string(),
367            email: "recipient@example.com".to_string(),
368        };
369        let cc_contact = Contact {
370            name: "CC".to_string(),
371            email: "cc@example.com".to_string(),
372        };
373        let bcc_contact = Contact {
374            name: "BCC".to_string(),
375            email: "bcc@example.com".to_string(),
376        };
377        let reply_contact = Contact {
378            name: "Reply".to_string(),
379            email: "reply@example.com".to_string(),
380        };
381
382        let email = Email::new()
383            .from(&sender)
384            .to(&recipient)
385            .cc(&cc_contact)
386            .bcc(&bcc_contact)
387            .reply_to(&reply_contact);
388
389        assert_eq!(email.from.as_ref().unwrap().email, "sender@example.com");
390        assert_eq!(email.to[0].email, "recipient@example.com");
391        assert_eq!(email.cc[0].email, "cc@example.com");
392        assert_eq!(email.bcc[0].email, "bcc@example.com");
393        assert_eq!(email.reply_to[0].email, "reply@example.com");
394    }
395}