hide mailparse error type behind own ParseError

This commit is contained in:
2026-10-10 22:30:55 +02:00
parent 275fd7a0fe
commit 37db780448
3 changed files with 66 additions and 32 deletions
+3 -3
View File
@@ -3,7 +3,7 @@
use mailparse::MailHeader;
use super::{Header, build_header};
use crate::{CowMailError, extend_over_newline};
use crate::{CowMailError, extend_over_newline, parse_error};
/// Find, iterate and edit a collection of headers by key.
///
@@ -310,7 +310,7 @@ pub(crate) fn build_headers<'a, H: Header<'a>>(
///
/// # Errors
///
/// [`CowMailError::MailParseError`] when [`mailparse::parse_headers`] rejects the block.
/// [`CowMailError::Parse`] when [`mailparse::parse_headers`] rejects the block.
///
/// # Examples
///
@@ -325,6 +325,6 @@ pub(crate) fn build_headers<'a, H: Header<'a>>(
/// assert_eq!(&raw[body_idx..], b"body\r\n");
/// ```
pub fn parse_headers<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(Vec<H>, usize), CowMailError> {
let (parsed, _) = mailparse::parse_headers(raw_data)?;
let (parsed, _) = mailparse::parse_headers(raw_data).map_err(parse_error)?;
build_headers(raw_data, &parsed)
}
+3 -3
View File
@@ -22,7 +22,7 @@ use std::fmt::{self, Display};
use charset::decode_latin1;
use mailparse::{MailHeader, parse_content_type};
use crate::{CowMailError, offset_in};
use crate::{CowMailError, offset_in, parse_error};
/// One header line, abstracted over how it is stored.
///
@@ -425,7 +425,7 @@ fn build_header<'a, H: Header<'a>>(
///
/// # Errors
///
/// [`CowMailError::MailParseError`] when [`mailparse::parse_header`] rejects the line.
/// [`CowMailError::Parse`] when [`mailparse::parse_header`] rejects the line.
///
/// # Examples
///
@@ -440,6 +440,6 @@ fn build_header<'a, H: Header<'a>>(
/// assert_eq!(&raw[next..], b"To: nobody\r\n");
/// ```
pub fn parse_header<'a, H: Header<'a>>(raw_data: &'a [u8]) -> Result<(H, usize), CowMailError> {
let (parsed, _) = mailparse::parse_header(raw_data)?;
let (parsed, _) = mailparse::parse_header(raw_data).map_err(parse_error)?;
build_header(raw_data, &parsed, None)
}
+60 -26
View File
@@ -17,7 +17,7 @@ use header::build_headers;
/// 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};
pub use mailparse::{DispositionType, ParsedContentDisposition, ParsedContentType};
use mailparse::{ParsedMail, body::Body, parse_content_disposition, parse_content_type};
/// Re-exported from [`mediatype`]. [`MediaType`] is the content type accepted by
@@ -30,27 +30,65 @@ use rand::{RngExt, distr::Alphanumeric, rng};
use std::borrow::Cow;
use std::ops::Deref;
/// A failure reported by the parser the crate uses internally.
///
/// The concrete error type is deliberately not part of this crate's API, so that the parser
/// can be changed or upgraded without breaking callers. [`Display`](std::fmt::Display) gives
/// the message, and [`source`](std::error::Error::source) the underlying error, for anyone who
/// wants to walk the chain down to e.g. the base64 decoder that rejected a body.
pub struct ParseError {
source: Box<dyn std::error::Error + Send + Sync + 'static>,
}
impl std::fmt::Debug for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.source, f)
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.source, f)
}
}
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&*self.source)
}
}
/// Wrap the error of an internal dependency into [`CowMailError::Parse`].
///
/// Used instead of a `From` impl, which would name the foreign error type in this crate's
/// public API — the whole point of hiding it.
pub(crate) fn parse_error(error: impl std::error::Error + Send + Sync + 'static) -> CowMailError {
CowMailError::Parse(ParseError {
source: Box::new(error),
})
}
/// Everything that can go wrong while parsing or modifying a message.
#[derive(Debug)]
pub enum CowMailError {
/// mailparse failed to parse the message. Carries its error unchanged.
MailParseError(MailParseError),
/// The parser rejected the message. Carries its error, see [`ParseError`].
Parse(ParseError),
/// 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.
/// Only reachable from [`parse_mail`], and only for a part the parser did split into
/// subparts — which it cannot do without a boundary. In other words: a broken invariant of
/// the bridge to the parser, not malformed input.
MissingBoundary,
/// A header line mailparse scanned does not point into the part it was scanned from.
/// A header line the parser 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
/// relies on every key and value being an exact subslice of its part. A parser 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.
/// A subpart the parser 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
/// positions are recovered from each part's raw bytes being an exact subslice of its
/// parent's.
SubpartNotASubslice,
/// The `Content-Type` header a modification needs is absent.
@@ -62,7 +100,7 @@ pub enum CowMailError {
impl std::error::Error for CowMailError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::MailParseError(error) => Some(error),
Self::Parse(error) => Some(error),
_ => None,
}
}
@@ -71,7 +109,7 @@ impl std::error::Error for CowMailError {
impl std::fmt::Display for CowMailError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MailParseError(error) => write!(f, "{}", error),
Self::Parse(error) => write!(f, "{}", error),
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"),
@@ -80,12 +118,6 @@ impl std::fmt::Display for CowMailError {
}
}
impl From<MailParseError> for CowMailError {
fn from(error: MailParseError) -> Self {
Self::MailParseError(error)
}
}
/// Which of the two text bodies of a message is meant.
///
/// A message can carry a `text/plain` body, a `text/html` one, or both as the alternatives of a
@@ -305,7 +337,7 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
///
/// # Errors
///
/// [`CowMailError::MailParseError`] if the body is not valid for the encoding it declares, or
/// [`CowMailError::Parse`] if the body is not valid for the encoding it declares, or
/// cannot be decoded with the declared charset.
///
/// # Examples
@@ -327,9 +359,11 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
.find_value("Content-Transfer-Encoding")
.map(|encoding| encoding.to_lowercase());
match Body::new(self.raw_body(), &ctype, &transfer_encoding) {
Body::Base64(body) | Body::QuotedPrintable(body) => Ok(body.get_decoded_as_string()?),
Body::SevenBit(body) | Body::EightBit(body) => Ok(body.get_as_string()?),
Body::Binary(body) => Ok(body.get_as_string()?),
Body::Base64(body) | Body::QuotedPrintable(body) => {
Ok(body.get_decoded_as_string().map_err(parse_error)?)
}
Body::SevenBit(body) | Body::EightBit(body) => Ok(body.get_as_string().map_err(parse_error)?),
Body::Binary(body) => Ok(body.get_as_string().map_err(parse_error)?),
}
}
@@ -340,14 +374,14 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
///
/// # Errors
///
/// [`CowMailError::MailParseError`] if the body is not valid for the encoding it declares.
/// [`CowMailError::Parse`] if the body is not valid for the encoding it declares.
fn get_content_raw(&self) -> Result<Vec<u8>, CowMailError> {
let ctype = self.content_type();
let transfer_encoding = self
.find_value("Content-Transfer-Encoding")
.map(|encoding| encoding.to_lowercase());
match Body::new(self.raw_body(), &ctype, &transfer_encoding) {
Body::Base64(body) | Body::QuotedPrintable(body) => Ok(body.get_decoded()?),
Body::Base64(body) | Body::QuotedPrintable(body) => Ok(body.get_decoded().map_err(parse_error)?),
Body::SevenBit(body) | Body::EightBit(body) => Ok(Vec::from(body.get_raw())),
Body::Binary(body) => Ok(Vec::from(body.get_raw())),
}
@@ -1337,7 +1371,7 @@ impl std::io::Read for CowMailReader<'_> {
///
/// # Errors
///
/// [`CowMailError::MailParseError`] when mailparse rejects the message; the other variants signal
/// [`CowMailError::Parse`] when mailparse rejects the message; the other variants signal
/// a broken invariant of the bridge to mailparse and should not occur.
///
/// # Examples
@@ -1350,7 +1384,7 @@ impl std::io::Read for CowMailReader<'_> {
/// assert_eq!(mail.find_value("Subject").unwrap(), "Hi");
/// ```
pub fn parse_mail<'a, H: Header<'a> + 'a>(raw_data: &'a [u8]) -> Result<CowMail<'a, H>, CowMailError> {
let parsed = mailparse::parse_mail(raw_data)?;
let parsed = mailparse::parse_mail(raw_data).map_err(parse_error)?;
build_mail(raw_data, &parsed, false)
}