1use std::fmt;
7
8use base64::{Engine as _, engine::general_purpose::STANDARD};
9
10#[derive(Debug, Clone)]
12pub struct Attachment {
13 pub filename: String,
15 pub content_type: String,
17 pub data: Vec<u8>,
19}
20
21impl Attachment {
22 pub fn new(filename: impl Into<String>, content_type: impl Into<String>, data: Vec<u8>) -> Self {
29 Self { filename: filename.into(), content_type: content_type.into(), data }
30 }
31
32 pub fn from_text(filename: impl Into<String>, content: impl Into<String>) -> Self {
38 Self {
39 filename: filename.into(),
40 content_type: "text/plain; charset=utf-8".to_string(),
41 data: content.into().into_bytes(),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Default)]
50pub struct EmailMessage {
51 pub from: String,
53 pub to: Vec<String>,
55 pub cc: Vec<String>,
57 pub bcc: Vec<String>,
59 pub subject: String,
61 pub body: Option<String>,
63 pub html_body: Option<String>,
65 pub attachments: Vec<Attachment>,
67 pub headers: Vec<(String, String)>,
69 pub message_id: Option<String>,
71 pub reply_to: Option<String>,
73}
74
75impl EmailMessage {
76 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn builder() -> EmailBuilder {
83 EmailBuilder::new()
84 }
85
86 pub fn to_bytes(&self) -> Vec<u8> {
90 let mut output = Vec::new();
91
92 self.write_headers(&mut output);
93
94 let boundary = self.generate_boundary();
95
96 if !self.attachments.is_empty() {
97 self.write_multipart_mixed(&mut output, &boundary);
98 }
99 else if self.html_body.is_some() && self.body.is_some() {
100 self.write_multipart_alternative(&mut output, &boundary);
101 }
102 else if let Some(ref html) = self.html_body {
103 self.write_html_only(&mut output, html);
104 }
105 else if let Some(ref text) = self.body {
106 self.write_text_only(&mut output, text);
107 }
108 else {
109 output.extend_from_slice(b"\r\n");
110 }
111
112 output
113 }
114
115 fn write_headers(&self, output: &mut Vec<u8>) {
117 if let Some(ref id) = self.message_id {
118 output.extend_from_slice(format!("Message-ID: <{}>\r\n", id).as_bytes());
119 }
120
121 output.extend_from_slice(format!("From: {}\r\n", self.encode_address(&self.from)).as_bytes());
122
123 if !self.to.is_empty() {
124 let to_encoded: Vec<String> = self.to.iter().map(|a| self.encode_address(a)).collect();
125 output.extend_from_slice(format!("To: {}\r\n", to_encoded.join(", ")).as_bytes());
126 }
127
128 if !self.cc.is_empty() {
129 let cc_encoded: Vec<String> = self.cc.iter().map(|a| self.encode_address(a)).collect();
130 output.extend_from_slice(format!("Cc: {}\r\n", cc_encoded.join(", ")).as_bytes());
131 }
132
133 if let Some(ref reply_to) = self.reply_to {
134 output.extend_from_slice(format!("Reply-To: {}\r\n", self.encode_address(reply_to)).as_bytes());
135 }
136
137 output.extend_from_slice(format!("Subject: {}\r\n", encode_subject(&self.subject)).as_bytes());
138
139 output.extend_from_slice(b"Date: ");
140 output.extend_from_slice(generate_date().as_bytes());
141 output.extend_from_slice(b"\r\n");
142
143 output.extend_from_slice(b"MIME-Version: 1.0\r\n");
144
145 for (name, value) in &self.headers {
146 output.extend_from_slice(format!("{}: {}\r\n", name, value).as_bytes());
147 }
148 }
149
150 fn encode_address(&self, addr: &str) -> String {
152 if addr.is_ascii() {
153 addr.to_string()
154 }
155 else {
156 if let Some(at_pos) = addr.rfind('@') {
157 let name_part = &addr[..at_pos];
158 let domain_part = &addr[at_pos..];
159 if name_part.is_ascii() { addr.to_string() } else { format!("{}{}", encode_subject(name_part), domain_part) }
160 }
161 else {
162 encode_subject(addr)
163 }
164 }
165 }
166
167 fn generate_boundary(&self) -> String {
169 let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos();
170 format!("----=_Part_{}_{}", timestamp, rand_suffix())
171 }
172
173 fn write_text_only(&self, output: &mut Vec<u8>, text: &str) {
175 if text.is_ascii() {
176 output.extend_from_slice(b"Content-Type: text/plain; charset=utf-8\r\n");
177 output.extend_from_slice(b"Content-Transfer-Encoding: 7bit\r\n");
178 output.extend_from_slice(b"\r\n");
179 output.extend_from_slice(text.as_bytes());
180 }
181 else {
182 output.extend_from_slice(b"Content-Type: text/plain; charset=utf-8\r\n");
183 output.extend_from_slice(b"Content-Transfer-Encoding: base64\r\n");
184 output.extend_from_slice(b"\r\n");
185 let encoded = STANDARD.encode(text.as_bytes());
186 for line in encoded.as_bytes().chunks(76) {
187 output.extend_from_slice(line);
188 output.extend_from_slice(b"\r\n");
189 }
190 }
191 }
192
193 fn write_html_only(&self, output: &mut Vec<u8>, html: &str) {
195 if html.is_ascii() {
196 output.extend_from_slice(b"Content-Type: text/html; charset=utf-8\r\n");
197 output.extend_from_slice(b"Content-Transfer-Encoding: 7bit\r\n");
198 output.extend_from_slice(b"\r\n");
199 output.extend_from_slice(html.as_bytes());
200 }
201 else {
202 output.extend_from_slice(b"Content-Type: text/html; charset=utf-8\r\n");
203 output.extend_from_slice(b"Content-Transfer-Encoding: base64\r\n");
204 output.extend_from_slice(b"\r\n");
205 let encoded = STANDARD.encode(html.as_bytes());
206 for line in encoded.as_bytes().chunks(76) {
207 output.extend_from_slice(line);
208 output.extend_from_slice(b"\r\n");
209 }
210 }
211 }
212
213 fn write_multipart_alternative(&self, output: &mut Vec<u8>, boundary: &str) {
215 output.extend_from_slice(format!("Content-Type: multipart/alternative; boundary=\"{}\"\r\n", boundary).as_bytes());
216 output.extend_from_slice(b"\r\n");
217
218 output.extend_from_slice(b"This is a multi-part message in MIME format.\r\n\r\n");
219
220 if let Some(ref text) = self.body {
221 output.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
222 self.write_text_only(output, text);
223 output.extend_from_slice(b"\r\n");
224 }
225
226 if let Some(ref html) = self.html_body {
227 output.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
228 self.write_html_only(output, html);
229 output.extend_from_slice(b"\r\n");
230 }
231
232 output.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
233 }
234
235 fn write_multipart_mixed(&self, output: &mut Vec<u8>, boundary: &str) {
237 output.extend_from_slice(format!("Content-Type: multipart/mixed; boundary=\"{}\"\r\n", boundary).as_bytes());
238 output.extend_from_slice(b"\r\n");
239
240 output.extend_from_slice(b"This is a multi-part message in MIME format.\r\n\r\n");
241
242 let has_content = self.body.is_some() || self.html_body.is_some();
243
244 if has_content {
245 output.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
246
247 if self.html_body.is_some() && self.body.is_some() {
248 let alt_boundary = format!("{}_alt", boundary);
249 output.extend_from_slice(
250 format!("Content-Type: multipart/alternative; boundary=\"{}\"\r\n", alt_boundary).as_bytes(),
251 );
252 output.extend_from_slice(b"\r\n");
253
254 if let Some(ref text) = self.body {
255 output.extend_from_slice(format!("--{}\r\n", alt_boundary).as_bytes());
256 self.write_text_only(output, text);
257 output.extend_from_slice(b"\r\n");
258 }
259
260 if let Some(ref html) = self.html_body {
261 output.extend_from_slice(format!("--{}\r\n", alt_boundary).as_bytes());
262 self.write_html_only(output, html);
263 output.extend_from_slice(b"\r\n");
264 }
265
266 output.extend_from_slice(format!("--{}--\r\n\r\n", alt_boundary).as_bytes());
267 }
268 else if let Some(ref html) = self.html_body {
269 self.write_html_only(output, html);
270 output.extend_from_slice(b"\r\n");
271 }
272 else if let Some(ref text) = self.body {
273 self.write_text_only(output, text);
274 output.extend_from_slice(b"\r\n");
275 }
276 }
277
278 for attachment in &self.attachments {
279 output.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
280 self.write_attachment(output, attachment);
281 }
282
283 output.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
284 }
285
286 fn write_attachment(&self, output: &mut Vec<u8>, attachment: &Attachment) {
288 let filename_encoded = encode_subject(&attachment.filename);
289 output.extend_from_slice(
290 format!("Content-Type: {}; name=\"{}\"\r\n", attachment.content_type, filename_encoded).as_bytes(),
291 );
292 output.extend_from_slice(b"Content-Transfer-Encoding: base64\r\n");
293 output.extend_from_slice(format!("Content-Disposition: attachment; filename=\"{}\"\r\n", filename_encoded).as_bytes());
294 output.extend_from_slice(b"\r\n");
295
296 let encoded = STANDARD.encode(&attachment.data);
297 for line in encoded.as_bytes().chunks(76) {
298 output.extend_from_slice(line);
299 output.extend_from_slice(b"\r\n");
300 }
301 output.extend_from_slice(b"\r\n");
302 }
303}
304
305impl fmt::Display for EmailMessage {
306 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307 write!(f, "{}", String::from_utf8_lossy(&self.to_bytes()))
308 }
309}
310
311#[derive(Debug, Clone, Default)]
315pub struct EmailBuilder {
316 message: EmailMessage,
317}
318
319impl EmailBuilder {
320 pub fn new() -> Self {
322 Self::default()
323 }
324
325 pub fn from(mut self, from: impl Into<String>) -> Self {
327 self.message.from = from.into();
328 self
329 }
330
331 pub fn to(mut self, to: impl Into<String>) -> Self {
333 self.message.to.push(to.into());
334 self
335 }
336
337 pub fn to_multiple(mut self, addresses: Vec<String>) -> Self {
339 self.message.to.extend(addresses);
340 self
341 }
342
343 pub fn cc(mut self, cc: impl Into<String>) -> Self {
345 self.message.cc.push(cc.into());
346 self
347 }
348
349 pub fn bcc(mut self, bcc: impl Into<String>) -> Self {
351 self.message.bcc.push(bcc.into());
352 self
353 }
354
355 pub fn subject(mut self, subject: impl Into<String>) -> Self {
357 self.message.subject = subject.into();
358 self
359 }
360
361 pub fn body(mut self, body: impl Into<String>) -> Self {
363 self.message.body = Some(body.into());
364 self
365 }
366
367 pub fn html_body(mut self, html: impl Into<String>) -> Self {
369 self.message.html_body = Some(html.into());
370 self
371 }
372
373 pub fn attachment(mut self, attachment: Attachment) -> Self {
375 self.message.attachments.push(attachment);
376 self
377 }
378
379 pub fn attachments(mut self, attachments: Vec<Attachment>) -> Self {
381 self.message.attachments.extend(attachments);
382 self
383 }
384
385 pub fn message_id(mut self, id: impl Into<String>) -> Self {
387 self.message.message_id = Some(id.into());
388 self
389 }
390
391 pub fn reply_to(mut self, reply_to: impl Into<String>) -> Self {
393 self.message.reply_to = Some(reply_to.into());
394 self
395 }
396
397 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
399 self.message.headers.push((name.into(), value.into()));
400 self
401 }
402
403 pub fn build(self) -> EmailMessage {
405 self.message
406 }
407}
408
409pub fn encode_subject(subject: &str) -> String {
413 if subject.is_ascii() {
414 subject.to_string()
415 }
416 else {
417 let encoded = STANDARD.encode(subject.as_bytes());
418 format!("=?UTF-8?B?{}?=", encoded)
419 }
420}
421
422pub fn generate_date() -> String {
424 chrono_now()
425}
426
427fn chrono_now() -> String {
429 use std::time::{SystemTime, UNIX_EPOCH};
430
431 let now = SystemTime::now();
432 let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
433 let secs = duration.as_secs();
434
435 let days_since_epoch = secs / 86400;
436 let secs_of_day = secs % 86400;
437 let hours = secs_of_day / 3600;
438 let minutes = (secs_of_day % 3600) / 60;
439 let seconds = secs_of_day % 60;
440
441 let (year, month, day, weekday) = days_to_date(days_since_epoch as i64);
442
443 let weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
444 let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
445
446 format!(
447 "{}, {:02} {} {:04} {:02}:{:02}:{:02} +0800",
448 weekdays[weekday as usize],
449 day,
450 months[(month - 1) as usize],
451 year,
452 hours,
453 minutes,
454 seconds
455 )
456}
457
458fn days_to_date(days: i64) -> (i32, i32, i32, i32) {
460 let mut year = 1970;
461 let mut days_left = days;
462
463 loop {
464 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
465 if days_left < days_in_year {
466 break;
467 }
468 days_left -= days_in_year;
469 year += 1;
470 }
471
472 let days_in_months = if is_leap_year(year) {
473 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
474 }
475 else {
476 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
477 };
478
479 let mut month = 1;
480 for &days_in_month in &days_in_months {
481 if days_left < days_in_month as i64 {
482 break;
483 }
484 days_left -= days_in_month as i64;
485 month += 1;
486 }
487
488 let day = days_left + 1;
489
490 let days_since_epoch = days;
491 let weekday = ((days_since_epoch + 3) % 7) as i32;
492 let weekday = if weekday < 0 { weekday + 7 } else { weekday };
493
494 (year, month, day as i32, weekday)
495}
496
497fn is_leap_year(year: i32) -> bool {
499 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
500}
501
502fn rand_suffix() -> String {
504 use std::time::{SystemTime, UNIX_EPOCH};
505
506 let now = SystemTime::now();
507 let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
508 let nanos = duration.subsec_nanos();
509
510 format!("{:08x}", nanos.wrapping_mul(2654435761))
511}