document crate, split CowMailError, re-export mailparse/mediatype/CowHeader

This commit is contained in:
2026-10-10 19:55:31 +02:00
parent a0e29a6c48
commit 6eb22c9a21
6 changed files with 172 additions and 16 deletions
+5
View File
@@ -1,4 +1,9 @@
//! Body transfer-encoding.
//!
//! [`encode`] takes the content of a part and returns the encoded bytes together with the
//! [`ContentTransferEncoding`] to declare for them. It picks the most compact of `7bit`,
//! `quoted-printable` and `base64`, so a plain ASCII body stays readable in the serialized message
//! while binary content ends up base64-encoded.
use std::fmt::{self, Display};
use std::mem;
+2
View File
@@ -1,3 +1,5 @@
//! RFC 2047 encoding and folding of header values.
use std::fmt::Write;
use email_encoding::headers::writer::EmailWriter;
+2
View File
@@ -1,3 +1,5 @@
//! The [`HeaderMap`] collection trait and header-block parsing.
use mailparse::MailHeader;
use super::{Header, build_header};
+11 -2
View File
@@ -1,3 +1,13 @@
//! Header lines.
//!
//! A header is abstracted by the [`Header`] trait, which is generic over the storage so that a
//! message can be parsed into whatever representation the caller needs. [`CowHeader`] is the
//! default implementation: it keeps the raw line — key, separator, value and trailing newline —
//! as a [`Cow`], and becomes owned only when the value is changed.
//!
//! [`HeaderMap`] is the collection side: finding, iterating and editing headers by key. It is
//! implemented for `Vec<H>` and for [`CowMail`](crate::CowMail).
mod encoding;
mod headers;
@@ -255,8 +265,7 @@ fn build_header<'a, H: Header<'a>>(
next: Option<&MailHeader<'a>>,
) -> Result<(H, usize), CowMailError> {
let offset_of = |bytes: &[u8]| {
offset_in(raw_data, bytes)
.ok_or_else(|| CowMailError::Generic("header is not a slice of its part".to_string()))
offset_in(raw_data, bytes).ok_or(CowMailError::HeaderNotASubslice)
};
let key = parsed.get_key_raw();
+64 -14
View File
@@ -1,40 +1,83 @@
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(missing_debug_implementations)]
#![warn(rustdoc::broken_intra_doc_links)]
/// Body transfer-encoding: choosing one, and the types describing it.
pub mod body;
/// Header lines: the [`Header`] trait, [`CowHeader`](header::CowHeader), and header-value encoding.
pub mod header;
#[cfg(test)]
mod tests;
pub use body::{BodyContent, ContentTransferEncoding};
use header::{CowHeader, build_headers};
pub use header::{Header, HeaderMap, parse_headers};
pub use header::{CowHeader, Header, HeaderMap, parse_header, parse_headers};
use header::build_headers;
use mailparse::{
DispositionType, MailParseError, ParsedContentDisposition, ParsedContentType, ParsedMail, body::Body,
parse_content_disposition, parse_content_type,
};
/// Re-exported from [`mailparse`], which parses the MIME structure for this crate: these types
/// appear in this crate's public API, so depending on `mailparse` separately is neither necessary
/// nor advisable — the version re-exported here is the one these signatures refer to.
pub use mailparse::{DispositionType, MailParseError, ParsedContentDisposition, ParsedContentType};
use mailparse::{ParsedMail, body::Body, parse_content_disposition, parse_content_type};
/// Re-exported from [`mediatype`]. [`MediaType`] is the content type accepted by
/// [`CowMail::set_content`] and [`CowMail::set_content_raw`], and [`names`] / [`values`] hold the
/// typed constants to build one with, so depending on `mediatype` separately is neither necessary
/// nor advisable.
pub use mediatype::{MediaType, names, values};
use mediatype::{MediaTypeBuf, Name, ReadParams, WriteParams, names::*, values::UTF_8};
use rand::{distr::Alphanumeric, rng, RngExt};
use rand::{RngExt, distr::Alphanumeric, rng};
use std::borrow::Cow;
use std::ops::Deref;
/// Everything that can go wrong while parsing or modifying a message.
#[derive(Debug)]
pub enum CowMailError {
Generic(String),
/// mailparse failed to parse the message. Carries its error unchanged.
MailParseError(MailParseError),
/// A part declares a `multipart/*` content type without a `boundary` parameter.
///
/// Only reachable from [`parse_mail`], and only for a part mailparse did split into subparts —
/// which it cannot do without a boundary. In other words: a broken invariant of the bridge to
/// mailparse, not malformed input.
MissingBoundary,
/// A header line mailparse scanned does not point into the part it was scanned from.
///
/// Header positions are recovered by comparing pointers (see the crate's `offset_in`), which
/// relies on every key and value being an exact subslice of its part. A mailparse that stops
/// doing that surfaces here instead of as a wrong index.
HeaderNotASubslice,
/// A subpart mailparse split out does not point into its parent's bytes.
///
/// The same invariant as [`HeaderNotASubslice`](Self::HeaderNotASubslice), one level up: part
/// positions are recovered from `ParsedMail::raw_bytes` being an exact subslice of its
/// parent's.
SubpartNotASubslice,
/// The `Content-Type` header a modification needs is absent.
///
/// Returned by [`CowMail::set_boundary`], which has no header to put the parameter on.
MissingContentTypeHeader,
}
impl std::error::Error for CowMailError {}
impl std::error::Error for CowMailError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::MailParseError(error) => Some(error),
_ => None,
}
}
}
impl std::fmt::Display for CowMailError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Generic(error) => write!(f, "{}", error),
Self::MailParseError(error) => write!(f, "{}", error),
Self::MissingContentTypeHeader => write!(f, "missing ContentType header"),
Self::MissingBoundary => write!(f, "multipart without a boundary parameter"),
Self::HeaderNotASubslice => write!(f, "header is not a slice of its part"),
Self::SubpartNotASubslice => write!(f, "subpart is not a slice of its parent"),
Self::MissingContentTypeHeader => write!(f, "missing Content-Type header"),
}
}
}
@@ -731,6 +774,14 @@ impl<'a> CowMailReader<'a> {
}
}
impl std::fmt::Debug for CowMailReader<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CowMailReader")
.field("current", &self.current)
.finish_non_exhaustive()
}
}
impl<'a, H: Header<'a>> From<&'a CowMail<'a, H>> for CowMailReader<'a> {
fn from(msg: &'a CowMail<'a, H>) -> Self {
CowMailReader {
@@ -849,11 +900,10 @@ fn build_mail<'a, H: Header<'a> + 'a>(
.ctype
.params
.get("boundary")
.ok_or_else(|| CowMailError::Generic("multipart without a boundary parameter".to_string()))?;
.ok_or(CowMailError::MissingBoundary)?;
let offset_of = |part: &ParsedMail<'a>| {
offset_in(raw_data, part.raw_bytes)
.ok_or_else(|| CowMailError::Generic("subpart is not a slice of its parent".to_string()))
offset_in(raw_data, part.raw_bytes).ok_or(CowMailError::SubpartNotASubslice)
};
// The preamble ends where the delimiter line before the first part begins.