daaki-message 0.2.0

RFC 5322 email message parser and builder
Documentation
//! RFC 5322 email message parser.
//!
//! Parses raw email message bytes into a structured [`ParsedEmail`] representation.
//! Handles partial messages (headers + truncated body) gracefully, extracting
//! whatever content is available.
//!
//! The parser is split into two layers:
//! - **Wire parser** ([`wire`]) — purely syntactic, splits bytes into structure
//!   (headers, body) without semantic interpretation.
//! - **Interpreter** ([`interpret`]) — converts wire-parsed data into the
//!   public [`ParsedEmail`] type by performing RFC 2047 decoding, address
//!   parsing, date parsing, MIME tree walking, and charset conversion.
//!
//! # References
//! - RFC 5322 Sections 2–3 (message syntax, header/body separation, field definitions)
//! - RFC 2045 Sections 5–6 (Content-Type, Content-Transfer-Encoding)
//! - RFC 2046 Sections 4–5 (discrete/composite media types, multipart boundaries)
//! - RFC 2047 Sections 2–4 (encoded-word syntax and decoding)
//! - RFC 2183 Section 2 (Content-Disposition field)
//! - RFC 2231 Sections 3–4 (MIME parameter continuations, charset/language)
//! - RFC 6532 Section 3 (internationalized UTF-8 headers)

mod interpret;
mod wire;

use crate::error::Error;
use crate::types::ParsedEmail;

// Re-export pub(crate) helpers used by other modules in the crate.
// `decode_encoded_words` is used by `crate::fuzz` behind `#[cfg(fuzzing)]`,
// which is not active during normal compilation — suppress the warning.
#[allow(unused_imports)]
pub(crate) use interpret::decode_encoded_words;
pub(crate) use interpret::find_paren_outside_quotes;
pub(crate) use interpret::normalize_display_name_phrase;
pub use interpret::parse_address_list;
pub(crate) use interpret::parse_rfc5322_date;
pub(crate) use interpret::strip_comments;

/// Parses raw email message bytes into a structured representation.
///
/// Handles partial messages (headers + truncated body) gracefully,
/// extracting whatever content is available. Returns [`Error::EmptyInput`]
/// for empty input. Malformed but still usable messages that omit the
/// required `From` header are accepted with an empty `from` list.
///
/// # References
/// - RFC 5322 Sections 2.1–2.3 (header/body separation, line length)
/// - RFC 5322 Sections 3.3–3.6 (date-time, address, field definitions)
/// - RFC 2045 Section 6 (Content-Transfer-Encoding decoding)
/// - RFC 2046 Sections 5.1–5.2 (multipart boundary parsing)
/// - RFC 2047 Sections 3–4 (encoded-word decoding in headers)
/// - RFC 2183 Section 2 (Content-Disposition parsing)
/// - RFC 2231 Sections 3–4 (parameter continuations, charset/language)
/// - RFC 6532 Section 3 (internationalized header fields)
pub fn parse_email(raw: &[u8]) -> Result<ParsedEmail, Error> {
    let wire_msg = wire::parse_wire(raw)?;
    interpret::interpret(&wire_msg, false)
}

/// Parses only the headers of a raw email message, skipping body/MIME processing.
///
/// This is faster than [`parse_email`] when only metadata is needed (e.g.,
/// building a message list). Body-related fields (`body_text`, `body_html`,
/// `attachments`) are always empty/`None`.
///
/// # References
/// - RFC 5322 Sections 2.1–2.2 (header/body separation, header folding)
/// - RFC 5322 Sections 3.3–3.6 (date-time, address, field definitions)
/// - RFC 2047 Sections 3–4 (encoded-word decoding in headers)
/// - RFC 6532 Section 3 (internationalized header fields)
pub fn parse_headers_only(raw: &[u8]) -> Result<ParsedEmail, Error> {
    let wire_msg = wire::parse_wire(raw)?;
    interpret::interpret(&wire_msg, true)
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

// Tests are placed in mod.rs because they exercise the full parse pipeline
// (wire → interpret) through the public `parse_email`/`parse_headers_only`
// entry points, plus direct calls to internal functions from both layers.
// Using `use` imports below makes all needed functions available to `super::*`
// in the test module.

#[cfg(test)]
#[path = "tests.rs"]
mod tests;