Skip to main content

rusmes_proto/
message.rs

1//! MIME message types
2
3use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::sync::Arc;
7use uuid::Uuid;
8
9/// Unique identifier for a message
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct MessageId(Uuid);
12
13impl MessageId {
14    /// Create a new unique message ID
15    pub fn new() -> Self {
16        Self(Uuid::new_v4())
17    }
18
19    /// Create a MessageId from an existing UUID
20    pub fn from_uuid(uuid: Uuid) -> Self {
21        Self(uuid)
22    }
23
24    /// Get the UUID value
25    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/// A streaming body for messages too large to hold in memory.
43///
44/// Backed by a pinned async reader behind a Mutex so the body can be
45/// cheaply cloned (Arc) while remaining safely pollable by one consumer
46/// at a time.
47///
48/// # One-shot semantics
49///
50/// `read_to_bytes` reads from the current stream position to EOF.
51/// Calling it a second time will return an empty buffer (the reader is at EOF).
52/// If you need to re-read the body, construct a new `LargeBody` via
53/// [`LargeBody::from_path`].
54#[derive(Clone)]
55pub struct LargeBody {
56    reader: Arc<tokio::sync::Mutex<std::pin::Pin<Box<dyn tokio::io::AsyncRead + Send + Sync>>>>,
57    /// Pre-known byte count (e.g. file size). Callers should not read past this.
58    size: u64,
59    /// Optional SHA-256 digest of the full body content.
60    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    /// Wrap an async reader of known size.
73    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    /// Open a file as a streaming body.
85    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    /// Pre-known byte count.
98    pub fn size(&self) -> u64 {
99        self.size
100    }
101
102    /// SHA-256 digest if available.
103    pub fn digest(&self) -> Option<&[u8; 32]> {
104        self.digest.as_ref()
105    }
106
107    /// Set the digest (used by callers that computed it during streaming).
108    pub fn set_digest(&mut self, digest: [u8; 32]) {
109        self.digest = Some(digest);
110    }
111
112    /// Read the entire body into memory.
113    ///
114    /// For large messages this is expensive — prefer streaming consumers.
115    ///
116    /// # One-shot semantics
117    ///
118    /// This reads from the current stream position to EOF. A second call will
119    /// return an empty buffer. Construct a new `LargeBody` to re-read.
120    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/// Message body - optimized for small and large messages
134#[derive(Clone, Debug)]
135pub enum MessageBody {
136    /// Small message stored in memory (<1MB)
137    Small(Bytes),
138    /// Large message backed by a streaming async reader.
139    Large(LargeBody),
140}
141
142impl MessageBody {
143    /// Choose `Small` or `Large` based on file size vs `threshold_bytes`.
144    ///
145    /// If the file fits within `threshold_bytes`, it is read into memory as `Small`.
146    /// Otherwise it is opened as a streaming `Large` body.
147    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/// MIME message structure
166#[derive(Debug, Clone)]
167pub struct MimeMessage {
168    headers: HeaderMap,
169    body: MessageBody,
170}
171
172impl MimeMessage {
173    /// Create a new MIME message
174    pub fn new(headers: HeaderMap, body: MessageBody) -> Self {
175        Self { headers, body }
176    }
177
178    /// Get message headers
179    pub fn headers(&self) -> &HeaderMap {
180        &self.headers
181    }
182
183    /// Get mutable message headers
184    pub fn headers_mut(&mut self) -> &mut HeaderMap {
185        &mut self.headers
186    }
187
188    /// Get message body
189    pub fn body(&self) -> &MessageBody {
190        &self.body
191    }
192
193    /// Extract text content from message body.
194    ///
195    /// For `Small` bodies this is infallible in the sense that any non-UTF-8
196    /// bytes would have caused an earlier parse failure; for `Large` bodies the
197    /// reader is consumed once (see [`LargeBody`] one-shot semantics).
198    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    /// Get message body size in bytes (body only, no headers).
210    ///
211    /// Use [`Self::size_with_headers`] when SIZE/quota semantics require the
212    /// full on-wire byte count including the serialized header block.
213    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    /// Get total message size in bytes (full on-wire form).
221    ///
222    /// Counts the serialized header block (every header line written as
223    /// `Name: Value\r\n`, then a final `\r\n` blank line separating headers
224    /// from the body) plus the body byte length.
225    ///
226    /// This is the value that should be reported for SMTP `SIZE` (RFC 1870),
227    /// IMAP `RFC822.SIZE` (RFC 9051), and quota accounting — all of which
228    /// describe the message as it appears on the wire, not just its body.
229    ///
230    /// Note: header values stored in [`HeaderMap`] have already been
231    /// unfolded; this calculation reports them in their re-folded canonical
232    /// CRLF-terminated single-line form. Callers that re-fold long header
233    /// values for transmission may emit slightly more bytes; this helper
234    /// gives the canonical lower-bound per-RFC count.
235    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                // "Name: Value\r\n"
240                total = total
241                    .saturating_add(name.len())
242                    .saturating_add(2) // ": "
243                    .saturating_add(value.len())
244                    .saturating_add(2); // "\r\n"
245            }
246        }
247        // Final blank line separator between headers and body.
248        total = total.saturating_add(2); // "\r\n"
249        total.saturating_add(self.body_size())
250    }
251
252    /// Get message size in bytes.
253    ///
254    /// Equivalent to [`Self::size_with_headers`] — preserved for backwards
255    /// compatibility with existing callers (storage backends, IMAP
256    /// `RFC822.SIZE`, JMAP `Email/get`, quota accounting). For body-only
257    /// counts use [`Self::body_size`] explicitly.
258    pub fn size(&self) -> usize {
259        self.size_with_headers()
260    }
261
262    /// Parse MIME message from raw bytes
263    pub fn parse_from_bytes(data: &[u8]) -> crate::error::Result<Self> {
264        let (headers_map, body_offset) = crate::mime::parse_headers(data)?;
265
266        // Convert to HeaderMap
267        let mut header_map = HeaderMap::new();
268        for (name, value) in headers_map {
269            header_map.insert(name, value);
270        }
271
272        // Extract body
273        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    /// Get Content-Type header parsed
285    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    /// Get Content-Transfer-Encoding
294    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    /// Parse multipart message into parts.
303    ///
304    /// For `Large` bodies, the body is read into memory first (acceptable
305    /// since multipart messages are rarely single-attachment multi-MB bodies).
306    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    /// Decode message body according to Content-Transfer-Encoding.
329    ///
330    /// For `Large` bodies, the body is read into memory before decoding.
331    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/// Message headers as a map
350#[derive(Debug, Clone, Default)]
351pub struct HeaderMap {
352    headers: HashMap<String, Vec<String>>,
353}
354
355impl HeaderMap {
356    /// Create a new empty header map
357    pub fn new() -> Self {
358        Self {
359            headers: HashMap::new(),
360        }
361    }
362
363    /// Insert a header value
364    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    /// Get all values for a header
371    pub fn get(&self, name: &str) -> Option<&[String]> {
372        self.headers.get(&name.to_lowercase()).map(|v| v.as_slice())
373    }
374
375    /// Get the first value for a header
376    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    /// Remove a header
381    pub fn remove(&mut self, name: &str) -> Option<Vec<String>> {
382        self.headers.remove(&name.to_lowercase())
383    }
384
385    /// Check if a header exists
386    pub fn contains(&self, name: &str) -> bool {
387        self.headers.contains_key(&name.to_lowercase())
388    }
389
390    /// Iterate over all headers
391    pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
392        self.headers.iter()
393    }
394
395    /// Parse headers from raw bytes with folding support
396    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    /// Fold a header value for proper line length
408    pub fn fold_value(value: &str) -> String {
409        crate::mime::fold_header(value, 78)
410    }
411
412    /// Unfold a header value
413    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        // body_size returns the body bytes only.
447        assert_eq!(msg.body_size(), 13);
448        // size() now reports headers + final CRLF + body. With no headers the
449        // header block is just the empty-line separator (2 bytes).
450        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        // Build a message with a deterministic single-header layout.
457        // Expected on-wire form:
458        //
459        //   subject: Hello\r\n
460        //   from: a@b\r\n
461        //   \r\n
462        //   Hi
463        //
464        // Bytes:
465        //   "subject: Hello\r\n" = 7 + 2 + 5 + 2 = 16
466        //   "from: a@b\r\n"      = 4 + 2 + 3 + 2 = 11
467        //   "\r\n"               = 2
468        //   body "Hi"            = 2
469        //   total                = 31
470        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        // Write 2 KiB with threshold 1 KiB → Large
517        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}