daaki-message 0.2.0

RFC 5322 email message parser and builder
Documentation
//! Message-ID and MIME boundary generation.
//!
//! # References
//! - RFC 5322 Section 3.6.4 (Message-ID uniqueness)
//! - RFC 2046 Section 5.1.1 (MIME boundary requirements)

use std::sync::atomic::Ordering;
use std::time::{SystemTime, UNIX_EPOCH};

use super::MSG_ID_COUNTER;

/// Generates a unique Message-ID using timestamp + PID + counter.
///
/// Format: `{hex}@{domain}` where hex is 16 bytes (32 hex chars).
///
/// # References
/// - RFC 5322 Section 3.6.4
pub(super) fn generate_message_id(domain: &str) -> String {
    let hex = generate_unique_hex();
    // RFC 5322 Section 3.6.4: id-right may be either dot-atom-text or a
    // bracketed no-fold-literal. Preserve a validated address-literal
    // domain verbatim so IPv6/general literals do not become invalid
    // flattened dot-atoms like `IPv6:2001:db8::1`.
    format!("{hex}@{domain}")
}

/// Generates a unique MIME boundary string.
///
/// # References
/// - RFC 2046 Section 5.1.1
pub(super) fn generate_boundary() -> String {
    let hex = generate_unique_hex();
    format!("----=_Part_{hex}")
}

/// Generates a MIME boundary that does not appear in `content`.
///
/// Tries up to 10 times with different random values. Falls back to a
/// counter-based boundary if all attempts collide (astronomically unlikely
/// with 32 hex chars, but avoids an infinite loop).
///
/// # References
/// - RFC 2046 Section 5.1.1: "The boundary delimiter MUST NOT appear within
///   the encapsulated material."
pub(super) fn generate_boundary_not_in(content: &[u8]) -> String {
    for _ in 0..10 {
        let boundary = generate_boundary();
        if !contains_boundary(content, &boundary) {
            return boundary;
        }
    }
    // Fallback: use a counter-based suffix to guarantee uniqueness.
    // Keep trying with incrementing counters until we find one that
    // doesn't collide (RFC 2046 Section 5.1.1: boundary MUST NOT
    // appear within the encapsulated material).
    for _ in 0..100 {
        let count = MSG_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
        let boundary = format!("----=_Part_fallback_{count:016x}");
        if !contains_boundary(content, &boundary) {
            return boundary;
        }
    }
    // Final fallback: keep trying counter-based boundaries until one
    // doesn't collide.  The counter is monotonically increasing so each
    // candidate is unique — the loop terminates quickly in practice.
    // RFC 2046 Section 5.1.1: boundary MUST NOT appear in the content.
    loop {
        let count = MSG_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
        let boundary = format!("----=_Part_fallback_{count:016x}");
        if !contains_boundary(content, &boundary) {
            return boundary;
        }
    }
}

/// Returns `true` if `content` contains the boundary string.
///
/// Checks for the boundary as it would appear in a MIME message, i.e.,
/// preceded by `--` (the MIME boundary delimiter prefix per RFC 2046
/// Section 5.1.1). Also checks for the bare boundary string to be safe.
///
/// # References
/// - RFC 2046 Section 5.1.1
pub(super) fn contains_boundary(content: &[u8], boundary: &str) -> bool {
    let boundary_bytes = boundary.as_bytes();
    // Check if the bare boundary string appears anywhere in the content.
    // This is conservative: a MIME parser only looks for CRLF + "--" + boundary,
    // but checking bare occurrence is safer and matches the RFC requirement that
    // the boundary "MUST NOT appear within the encapsulated material".
    content
        .windows(boundary_bytes.len())
        .any(|w| w == boundary_bytes)
}

/// Generates 32 hex characters (16 bytes) of crypto-random data.
///
/// Uses `/dev/urandom` for crypto-quality randomness. Falls back to
/// timestamp + PID + counter if `/dev/urandom` is unavailable.
fn generate_unique_hex() -> String {
    // Try crypto-random first (RFC 5322 Section 3.6.4 — Message-ID uniqueness)
    let mut buf = [0u8; 16];
    if read_urandom(&mut buf).is_ok() {
        return buf.iter().fold(String::with_capacity(32), |mut s, b| {
            use std::fmt::Write;
            let _ = write!(s, "{b:02x}");
            s
        });
    }

    // Fallback: timestamp + PID + counter (deterministic but unique)
    #[allow(clippy::cast_possible_truncation)]
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos() as u64;
    let count = MSG_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
    let pid = u64::from(std::process::id());
    format!("{nanos:016x}{pid:08x}{count:08x}")
}

/// Reads crypto-random bytes from `/dev/urandom`.
fn read_urandom(buf: &mut [u8]) -> std::io::Result<()> {
    use std::io::Read;
    let mut f = std::fs::File::open("/dev/urandom")?;
    f.read_exact(buf)
}