1use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::sync::Arc;
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct MessageId(Uuid);
12
13impl MessageId {
14 pub fn new() -> Self {
16 Self(Uuid::new_v4())
17 }
18
19 pub fn from_uuid(uuid: Uuid) -> Self {
21 Self(uuid)
22 }
23
24 pub fn as_uuid(&self) -> &Uuid {
26 &self.0
27 }
28}
29
30impl Default for MessageId {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl std::fmt::Display for MessageId {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(f, "{}", self.0)
39 }
40}
41
42#[derive(Clone)]
55pub struct LargeBody {
56 reader: Arc<tokio::sync::Mutex<std::pin::Pin<Box<dyn tokio::io::AsyncRead + Send + Sync>>>>,
57 size: u64,
59 digest: Option<[u8; 32]>,
61}
62
63impl std::fmt::Debug for LargeBody {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.debug_struct("LargeBody")
66 .field("size", &self.size)
67 .finish()
68 }
69}
70
71impl LargeBody {
72 pub fn from_reader<R: tokio::io::AsyncRead + Send + Sync + 'static>(
74 reader: R,
75 size: u64,
76 ) -> Self {
77 Self {
78 reader: Arc::new(tokio::sync::Mutex::new(Box::pin(reader))),
79 size,
80 digest: None,
81 }
82 }
83
84 pub async fn from_path(path: &std::path::Path) -> crate::error::Result<Self> {
86 let file = tokio::fs::File::open(path)
87 .await
88 .map_err(|e| crate::error::MailError::Parse(format!("cannot open large body: {e}")))?;
89 let size = file
90 .metadata()
91 .await
92 .map_err(|e| crate::error::MailError::Parse(format!("cannot stat large body: {e}")))?
93 .len();
94 Ok(Self::from_reader(file, size))
95 }
96
97 pub fn size(&self) -> u64 {
99 self.size
100 }
101
102 pub fn digest(&self) -> Option<&[u8; 32]> {
104 self.digest.as_ref()
105 }
106
107 pub fn set_digest(&mut self, digest: [u8; 32]) {
109 self.digest = Some(digest);
110 }
111
112 pub async fn read_to_bytes(&self) -> crate::error::Result<Bytes> {
121 use tokio::io::AsyncReadExt;
122 let mut guard = self.reader.lock().await;
123 let mut buf = Vec::new();
124 guard
125 .as_mut()
126 .read_to_end(&mut buf)
127 .await
128 .map_err(|e| crate::error::MailError::Parse(format!("read error: {e}")))?;
129 Ok(Bytes::from(buf))
130 }
131}
132
133#[derive(Clone, Debug)]
135pub enum MessageBody {
136 Small(Bytes),
138 Large(LargeBody),
140}
141
142impl MessageBody {
143 pub async fn from_path_with_threshold(
148 path: &std::path::Path,
149 threshold_bytes: u64,
150 ) -> crate::error::Result<Self> {
151 let meta = tokio::fs::metadata(path)
152 .await
153 .map_err(|e| crate::error::MailError::Parse(format!("stat failed: {e}")))?;
154 if meta.len() <= threshold_bytes {
155 let data = tokio::fs::read(path)
156 .await
157 .map_err(|e| crate::error::MailError::Parse(format!("read failed: {e}")))?;
158 Ok(MessageBody::Small(Bytes::from(data)))
159 } else {
160 Ok(MessageBody::Large(LargeBody::from_path(path).await?))
161 }
162 }
163}
164
165#[derive(Debug, Clone)]
167pub struct MimeMessage {
168 headers: HeaderMap,
169 body: MessageBody,
170}
171
172impl MimeMessage {
173 pub fn new(headers: HeaderMap, body: MessageBody) -> Self {
175 Self { headers, body }
176 }
177
178 pub fn headers(&self) -> &HeaderMap {
180 &self.headers
181 }
182
183 pub fn headers_mut(&mut self) -> &mut HeaderMap {
185 &mut self.headers
186 }
187
188 pub fn body(&self) -> &MessageBody {
190 &self.body
191 }
192
193 pub async fn extract_text(&self) -> crate::error::Result<String> {
199 match &self.body {
200 MessageBody::Small(bytes) => String::from_utf8(bytes.to_vec())
201 .map_err(|e| crate::error::MailError::Parse(e.to_string())),
202 MessageBody::Large(large) => {
203 let data = large.read_to_bytes().await?;
204 Ok(String::from_utf8_lossy(&data).into_owned())
205 }
206 }
207 }
208
209 pub fn body_size(&self) -> usize {
214 match &self.body {
215 MessageBody::Small(bytes) => bytes.len(),
216 MessageBody::Large(large) => large.size() as usize,
217 }
218 }
219
220 pub fn size_with_headers(&self) -> usize {
236 let mut total = 0usize;
237 for (name, values) in self.headers.iter() {
238 for value in values {
239 total = total
241 .saturating_add(name.len())
242 .saturating_add(2) .saturating_add(value.len())
244 .saturating_add(2); }
246 }
247 total = total.saturating_add(2); total.saturating_add(self.body_size())
250 }
251
252 pub fn size(&self) -> usize {
259 self.size_with_headers()
260 }
261
262 pub fn parse_from_bytes(data: &[u8]) -> crate::error::Result<Self> {
264 let (headers_map, body_offset) = crate::mime::parse_headers(data)?;
265
266 let mut header_map = HeaderMap::new();
268 for (name, value) in headers_map {
269 header_map.insert(name, value);
270 }
271
272 let body_bytes = if body_offset < data.len() {
274 Bytes::copy_from_slice(&data[body_offset..])
275 } else {
276 Bytes::new()
277 };
278
279 let body = MessageBody::Small(body_bytes);
280
281 Ok(MimeMessage::new(header_map, body))
282 }
283
284 pub fn content_type(&self) -> crate::error::Result<Option<crate::mime::ContentType>> {
286 if let Some(ct) = self.headers.get_first("content-type") {
287 Ok(Some(crate::mime::ContentType::parse(ct)?))
288 } else {
289 Ok(None)
290 }
291 }
292
293 pub fn content_transfer_encoding(&self) -> crate::mime::ContentTransferEncoding {
295 if let Some(cte) = self.headers.get_first("content-transfer-encoding") {
296 crate::mime::ContentTransferEncoding::parse(cte.trim())
297 } else {
298 crate::mime::ContentTransferEncoding::SevenBit
299 }
300 }
301
302 pub async fn parse_multipart(&self) -> crate::error::Result<Vec<crate::mime::MimePart>> {
307 let content_type = self
308 .content_type()?
309 .ok_or_else(|| crate::error::MailError::Parse("No Content-Type header".to_string()))?;
310
311 if !content_type.is_multipart() {
312 return Err(crate::error::MailError::Parse(
313 "Not a multipart message".to_string(),
314 ));
315 }
316
317 let boundary = content_type.boundary().ok_or_else(|| {
318 crate::error::MailError::Parse("No boundary in multipart".to_string())
319 })?;
320
321 let bytes = match &self.body {
322 MessageBody::Small(bytes) => bytes.clone(),
323 MessageBody::Large(large) => large.read_to_bytes().await?,
324 };
325 crate::mime::split_multipart(&bytes, boundary)
326 }
327
328 pub async fn decode_body(&self) -> crate::error::Result<Vec<u8>> {
332 let encoding = self.content_transfer_encoding();
333
334 let bytes = match &self.body {
335 MessageBody::Small(bytes) => bytes.clone(),
336 MessageBody::Large(large) => large.read_to_bytes().await?,
337 };
338
339 match encoding {
340 crate::mime::ContentTransferEncoding::Base64 => crate::mime::decode_base64(&bytes),
341 crate::mime::ContentTransferEncoding::QuotedPrintable => {
342 crate::mime::decode_quoted_printable(&bytes)
343 }
344 _ => Ok(bytes.to_vec()),
345 }
346 }
347}
348
349#[derive(Debug, Clone, Default)]
351pub struct HeaderMap {
352 headers: HashMap<String, Vec<String>>,
353}
354
355impl HeaderMap {
356 pub fn new() -> Self {
358 Self {
359 headers: HashMap::new(),
360 }
361 }
362
363 pub fn insert(&mut self, name: impl Into<String>, value: impl Into<String>) {
365 let name = name.into().to_lowercase();
366 let value = value.into();
367 self.headers.entry(name).or_default().push(value);
368 }
369
370 pub fn get(&self, name: &str) -> Option<&[String]> {
372 self.headers.get(&name.to_lowercase()).map(|v| v.as_slice())
373 }
374
375 pub fn get_first(&self, name: &str) -> Option<&str> {
377 self.get(name).and_then(|v| v.first().map(|s| s.as_str()))
378 }
379
380 pub fn remove(&mut self, name: &str) -> Option<Vec<String>> {
382 self.headers.remove(&name.to_lowercase())
383 }
384
385 pub fn contains(&self, name: &str) -> bool {
387 self.headers.contains_key(&name.to_lowercase())
388 }
389
390 pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
392 self.headers.iter()
393 }
394
395 pub fn parse_from_bytes(data: &[u8]) -> crate::error::Result<(Self, usize)> {
397 let (headers_map, offset) = crate::mime::parse_headers(data)?;
398
399 let mut header_map = HeaderMap::new();
400 for (name, value) in headers_map {
401 header_map.insert(name, value);
402 }
403
404 Ok((header_map, offset))
405 }
406
407 pub fn fold_value(value: &str) -> String {
409 crate::mime::fold_header(value, 78)
410 }
411
412 pub fn unfold_value(value: &str) -> String {
414 crate::mime::unfold_header(value)
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_message_id_unique() {
424 let id1 = MessageId::new();
425 let id2 = MessageId::new();
426 assert_ne!(id1, id2);
427 }
428
429 #[test]
430 fn test_header_map_case_insensitive() {
431 let mut headers = HeaderMap::new();
432 headers.insert("Content-Type", "text/plain");
433 headers.insert("content-type", "text/html");
434
435 let values = headers.get("CONTENT-TYPE").unwrap();
436 assert_eq!(values.len(), 2);
437 assert!(values.contains(&"text/plain".to_string()));
438 assert!(values.contains(&"text/html".to_string()));
439 }
440
441 #[test]
442 fn test_small_message_body() {
443 let body = MessageBody::Small(Bytes::from("Hello, World!"));
444 let headers = HeaderMap::new();
445 let msg = MimeMessage::new(headers, body);
446 assert_eq!(msg.body_size(), 13);
448 assert_eq!(msg.size(), 15);
451 assert_eq!(msg.size(), msg.size_with_headers());
452 }
453
454 #[test]
455 fn size_with_headers_known_message() {
456 let mut headers = HeaderMap::new();
471 headers.insert("subject", "Hello");
472 headers.insert("from", "a@b");
473 let body = MessageBody::Small(Bytes::from("Hi"));
474 let msg = MimeMessage::new(headers, body);
475 assert_eq!(msg.body_size(), 2);
476 assert_eq!(msg.size_with_headers(), 31);
477 assert_eq!(msg.size(), 31);
478 }
479}
480
481#[cfg(test)]
482mod large_body_tests {
483 use super::*;
484 use std::env::temp_dir;
485
486 #[tokio::test]
487 async fn test_largebody_from_path_roundtrip() {
488 let mut path = temp_dir();
489 path.push(format!("rusmes_test_large_{}.bin", uuid::Uuid::new_v4()));
490 tokio::fs::write(&path, b"hello streaming world")
491 .await
492 .unwrap();
493 let large = LargeBody::from_path(&path).await.unwrap();
494 assert_eq!(large.size(), 21);
495 let data = large.read_to_bytes().await.unwrap();
496 assert_eq!(&data[..], b"hello streaming world");
497 let _ = tokio::fs::remove_file(&path).await;
498 }
499
500 #[tokio::test]
501 async fn test_messagebody_threshold_chooses_small() {
502 let mut path = temp_dir();
503 path.push(format!("rusmes_test_small_{}.bin", uuid::Uuid::new_v4()));
504 tokio::fs::write(&path, b"tiny").await.unwrap();
505 let body = MessageBody::from_path_with_threshold(&path, 1024)
506 .await
507 .unwrap();
508 assert!(matches!(body, MessageBody::Small(_)));
509 let _ = tokio::fs::remove_file(&path).await;
510 }
511
512 #[tokio::test]
513 async fn test_messagebody_threshold_chooses_large() {
514 let mut path = temp_dir();
515 path.push(format!("rusmes_test_thresh_{}.bin", uuid::Uuid::new_v4()));
516 let data = vec![0u8; 2048];
518 tokio::fs::write(&path, &data).await.unwrap();
519 let body = MessageBody::from_path_with_threshold(&path, 1024)
520 .await
521 .unwrap();
522 assert!(matches!(body, MessageBody::Large(_)));
523 let _ = tokio::fs::remove_file(&path).await;
524 }
525
526 #[tokio::test]
527 async fn test_largebody_extract_text() {
528 let mut path = temp_dir();
529 path.push(format!("rusmes_test_text_{}.txt", uuid::Uuid::new_v4()));
530 tokio::fs::write(&path, b"Hello, Large World!")
531 .await
532 .unwrap();
533 let large = LargeBody::from_path(&path).await.unwrap();
534 let msg = MimeMessage::new(HeaderMap::new(), MessageBody::Large(large));
535 let text = msg.extract_text().await.unwrap();
536 assert_eq!(text, "Hello, Large World!");
537 let _ = tokio::fs::remove_file(&path).await;
538 }
539}