avila_cell/
message.rs

1//! Estrutura de mensagem de email
2
3use crate::{EmailAddress, mime::{MimePart, MultipartBuilder}};
4use avila_time::DateTime;
5use std::collections::HashMap;
6
7/// Complete email message
8#[derive(Debug, Clone)]
9pub struct Email {
10    /// Unique message ID
11    pub id: String,
12    /// Sender
13    pub from: EmailAddress,
14    /// Recipients
15    pub to: Vec<EmailAddress>,
16    /// Carbon copy
17    pub cc: Vec<EmailAddress>,
18    /// Blind carbon copy
19    pub bcc: Vec<EmailAddress>,
20    /// Subject
21    pub subject: String,
22    /// Message body (plain text)
23    pub body: String,
24    /// HTML body (optional)
25    pub html_body: Option<String>,
26    /// Custom headers
27    pub headers: HashMap<String, String>,
28    /// Send date
29    pub date: DateTime,
30    /// Attachments
31    pub attachments: Vec<Attachment>,
32}
33
34/// Email attachment
35#[derive(Debug, Clone)]
36pub struct Attachment {
37    /// Filename
38    pub filename: String,
39    /// MIME type
40    pub content_type: String,
41    /// Content (base64 encoded)
42    pub content: Vec<u8>,
43}
44
45impl Email {
46    /// Creates new message
47    pub fn new(from: EmailAddress, to: Vec<EmailAddress>, subject: String, body: String) -> Self {
48        Self {
49            id: Self::generate_message_id(),
50            from,
51            to,
52            cc: Vec::new(),
53            bcc: Vec::new(),
54            subject,
55            body,
56            html_body: None,
57            headers: HashMap::new(),
58            date: DateTime::now(),
59            attachments: Vec::new(),
60        }
61    }
62
63    /// Generates unique Message-ID
64    fn generate_message_id() -> String {
65        use avila_time::DateTime;
66        let timestamp = DateTime::now().timestamp();
67        let random: u32 = (timestamp % 1_000_000) as u32;
68        format!("<{}.{}@avila.inc>", timestamp, random)
69    }
70
71    /// Adds CC recipient
72    pub fn add_cc(&mut self, address: EmailAddress) {
73        self.cc.push(address);
74    }
75
76    /// Adds BCC recipient
77    pub fn add_bcc(&mut self, address: EmailAddress) {
78        self.bcc.push(address);
79    }
80
81    /// Adds attachment
82    pub fn add_attachment(&mut self, attachment: Attachment) {
83        self.attachments.push(attachment);
84    }
85
86    /// Sets HTML body
87    pub fn set_html_body(&mut self, html: String) {
88        self.html_body = Some(html);
89    }
90
91    /// Adds custom header
92    pub fn add_header(&mut self, key: String, value: String) {
93        self.headers.insert(key, value);
94    }
95
96    /// Converts to RFC 5322 format (email wire format)
97    pub fn to_rfc5322(&self) -> String {
98        if self.html_body.is_some() || !self.attachments.is_empty() {
99            // Use simple format as fallback for now
100            self.to_simple_format()
101        } else {
102            self.to_simple_format()
103        }
104    }
105
106    /// Converts to MIME multipart format
107    pub fn to_mime(&self) -> String {
108        let mut headers = String::new();
109
110        // Basic headers
111        headers.push_str(&format!("Message-ID: {}\r\n", self.id));
112        headers.push_str(&format!("From: {}\r\n", self.from.to_rfc5322()));
113        headers.push_str(&format!("To: {}\r\n",
114            self.to.iter().map(|e| e.to_rfc5322()).collect::<Vec<_>>().join(", ")));
115
116        if !self.cc.is_empty() {
117            headers.push_str(&format!("Cc: {}\r\n",
118                self.cc.iter().map(|e| e.to_rfc5322()).collect::<Vec<_>>().join(", ")));
119        }
120
121        headers.push_str(&format!("Subject: {}\r\n", self.subject));
122        headers.push_str(&format!("Date: {}\r\n", self.date.to_rfc2822()));
123        headers.push_str("MIME-Version: 1.0\r\n");
124
125        // Custom headers
126        for (key, value) in &self.headers {
127            headers.push_str(&format!("{}: {}\r\n", key, value));
128        }
129
130        // Build multipart body
131        if !self.attachments.is_empty() || self.html_body.is_some() {
132            let mut builder = if self.html_body.is_some() {
133                // multipart/alternative for text + HTML
134                let mut alt = MultipartBuilder::alternative()
135                    .add_part(MimePart::text(&self.body));
136
137                if let Some(ref html) = self.html_body {
138                    alt = alt.add_part(MimePart::html(html));
139                }
140
141                if self.attachments.is_empty() {
142                    headers.push_str(&format!("Content-Type: {}\r\n", alt.content_type()));
143                    headers.push_str("\r\n");
144                    headers.push_str(&alt.build());
145                    return headers;
146                }
147
148                // Wrap in multipart/mixed for attachments
149                let mixed = MultipartBuilder::mixed();
150                mixed
151            } else {
152                MultipartBuilder::mixed()
153                    .add_part(MimePart::text(&self.body))
154            };
155
156            // Add attachments
157            for attachment in &self.attachments {
158                builder = builder.add_part(MimePart::attachment(
159                    &attachment.filename,
160                    &attachment.content_type,
161                    attachment.content.clone(),
162                ));
163            }
164
165            headers.push_str(&format!("Content-Type: {}\r\n", builder.content_type()));
166            headers.push_str("\r\n");
167            headers.push_str(&builder.build());
168        } else {
169            // Simple text email
170            headers.push_str("Content-Type: text/plain; charset=utf-8\r\n");
171            headers.push_str("Content-Transfer-Encoding: 8bit\r\n");
172            headers.push_str("\r\n");
173            headers.push_str(&self.body);
174        }
175
176        headers
177    }
178
179    fn to_simple_format(&self) -> String {
180        let mut message = String::new();
181
182        message.push_str(&format!("Message-ID: {}\r\n", self.id));
183        message.push_str(&format!("From: {}\r\n", self.from));
184        message.push_str(&format!("To: {}\r\n",
185            self.to.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", ")));
186
187        if !self.cc.is_empty() {
188            message.push_str(&format!("Cc: {}\r\n",
189                self.cc.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", ")));
190        }
191
192        message.push_str(&format!("Subject: {}\r\n", self.subject));
193        message.push_str(&format!("Date: {}\r\n", self.date.to_rfc2822()));
194
195        // Custom headers
196        for (key, value) in &self.headers {
197            message.push_str(&format!("{}: {}\r\n", key, value));
198        }
199
200        // Body
201        message.push_str("\r\n");
202        message.push_str(&self.body);
203
204        message
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_email_creation() {
214        let from = EmailAddress::new("sender@example.com").unwrap();
215        let to = vec![EmailAddress::new("recipient@example.com").unwrap()];
216
217        let email = Email::new(from, to, "Test".to_string(), "Hello!".to_string());
218
219        assert_eq!(email.subject, "Test");
220        assert_eq!(email.body, "Hello!");
221    }
222
223    #[test]
224    fn test_rfc5322_format() {
225        let from = EmailAddress::new("sender@example.com").unwrap();
226        let to = vec![EmailAddress::new("recipient@example.com").unwrap()];
227
228        let email = Email::new(from, to, "Test".to_string(), "Hello!".to_string());
229        let rfc = email.to_rfc5322();
230
231        assert!(rfc.contains("From: sender@example.com"));
232        assert!(rfc.contains("To: recipient@example.com"));
233        assert!(rfc.contains("Subject: Test"));
234        assert!(rfc.contains("Hello!"));
235    }
236}