1use crate::{EmailAddress, mime::{MimePart, MultipartBuilder}};
4use avila_time::DateTime;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct Email {
10 pub id: String,
12 pub from: EmailAddress,
14 pub to: Vec<EmailAddress>,
16 pub cc: Vec<EmailAddress>,
18 pub bcc: Vec<EmailAddress>,
20 pub subject: String,
22 pub body: String,
24 pub html_body: Option<String>,
26 pub headers: HashMap<String, String>,
28 pub date: DateTime,
30 pub attachments: Vec<Attachment>,
32}
33
34#[derive(Debug, Clone)]
36pub struct Attachment {
37 pub filename: String,
39 pub content_type: String,
41 pub content: Vec<u8>,
43}
44
45impl Email {
46 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 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 pub fn add_cc(&mut self, address: EmailAddress) {
73 self.cc.push(address);
74 }
75
76 pub fn add_bcc(&mut self, address: EmailAddress) {
78 self.bcc.push(address);
79 }
80
81 pub fn add_attachment(&mut self, attachment: Attachment) {
83 self.attachments.push(attachment);
84 }
85
86 pub fn set_html_body(&mut self, html: String) {
88 self.html_body = Some(html);
89 }
90
91 pub fn add_header(&mut self, key: String, value: String) {
93 self.headers.insert(key, value);
94 }
95
96 pub fn to_rfc5322(&self) -> String {
98 if self.html_body.is_some() || !self.attachments.is_empty() {
99 self.to_simple_format()
101 } else {
102 self.to_simple_format()
103 }
104 }
105
106 pub fn to_mime(&self) -> String {
108 let mut headers = String::new();
109
110 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 for (key, value) in &self.headers {
127 headers.push_str(&format!("{}: {}\r\n", key, value));
128 }
129
130 if !self.attachments.is_empty() || self.html_body.is_some() {
132 let mut builder = if self.html_body.is_some() {
133 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 let mixed = MultipartBuilder::mixed();
150 mixed
151 } else {
152 MultipartBuilder::mixed()
153 .add_part(MimePart::text(&self.body))
154 };
155
156 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 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 for (key, value) in &self.headers {
197 message.push_str(&format!("{}: {}\r\n", key, value));
198 }
199
200 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}