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 mailparse::MailHeader;
use super::{Header, build_header}; 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. /// Find, iterate and edit a collection of headers by key.
/// ///
@@ -310,7 +310,7 @@ pub(crate) fn build_headers<'a, H: Header<'a>>(
/// ///
/// # Errors /// # Errors
/// ///
/// [`CowMailError::MailParseError`] when [`mailparse::parse_headers`] rejects the block. /// [`CowMailError::Parse`] when [`mailparse::parse_headers`] rejects the block.
/// ///
/// # Examples /// # Examples
/// ///
@@ -325,6 +325,6 @@ pub(crate) fn build_headers<'a, H: Header<'a>>(
/// assert_eq!(&raw[body_idx..], b"body\r\n"); /// 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> { 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) build_headers(raw_data, &parsed)
} }
+3 -3
View File
@@ -22,7 +22,7 @@ use std::fmt::{self, Display};
use charset::decode_latin1; use charset::decode_latin1;
use mailparse::{MailHeader, parse_content_type}; 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. /// One header line, abstracted over how it is stored.
/// ///
@@ -425,7 +425,7 @@ fn build_header<'a, H: Header<'a>>(
/// ///
/// # Errors /// # Errors
/// ///
/// [`CowMailError::MailParseError`] when [`mailparse::parse_header`] rejects the line. /// [`CowMailError::Parse`] when [`mailparse::parse_header`] rejects the line.
/// ///
/// # Examples /// # Examples
/// ///
@@ -440,6 +440,6 @@ fn build_header<'a, H: Header<'a>>(
/// assert_eq!(&raw[next..], b"To: nobody\r\n"); /// 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> { 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) 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 /// 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 /// 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. /// 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}; use mailparse::{ParsedMail, body::Body, parse_content_disposition, parse_content_type};
/// Re-exported from [`mediatype`]. [`MediaType`] is the content type accepted by /// 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::borrow::Cow;
use std::ops::Deref; 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. /// Everything that can go wrong while parsing or modifying a message.
#[derive(Debug)] #[derive(Debug)]
pub enum CowMailError { pub enum CowMailError {
/// mailparse failed to parse the message. Carries its error unchanged. /// The parser rejected the message. Carries its error, see [`ParseError`].
MailParseError(MailParseError), Parse(ParseError),
/// A part declares a `multipart/*` content type without a `boundary` parameter. /// 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 — /// Only reachable from [`parse_mail`], and only for a part the parser did split into
/// which it cannot do without a boundary. In other words: a broken invariant of the bridge to /// subparts — which it cannot do without a boundary. In other words: a broken invariant of
/// mailparse, not malformed input. /// the bridge to the parser, not malformed input.
MissingBoundary, 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 /// 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. /// doing that surfaces here instead of as a wrong index.
HeaderNotASubslice, 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 /// 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. /// parent's.
SubpartNotASubslice, SubpartNotASubslice,
/// The `Content-Type` header a modification needs is absent. /// The `Content-Type` header a modification needs is absent.
@@ -62,7 +100,7 @@ pub enum CowMailError {
impl std::error::Error for CowMailError { impl std::error::Error for CowMailError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self { match self {
Self::MailParseError(error) => Some(error), Self::Parse(error) => Some(error),
_ => None, _ => None,
} }
} }
@@ -71,7 +109,7 @@ impl std::error::Error for CowMailError {
impl std::fmt::Display for CowMailError { impl std::fmt::Display for CowMailError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::MailParseError(error) => write!(f, "{}", error), Self::Parse(error) => write!(f, "{}", error),
Self::MissingBoundary => write!(f, "multipart without a boundary parameter"), Self::MissingBoundary => write!(f, "multipart without a boundary parameter"),
Self::HeaderNotASubslice => write!(f, "header is not a slice of its part"), 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::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. /// 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 /// 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 /// # 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. /// cannot be decoded with the declared charset.
/// ///
/// # Examples /// # Examples
@@ -327,9 +359,11 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
.find_value("Content-Transfer-Encoding") .find_value("Content-Transfer-Encoding")
.map(|encoding| encoding.to_lowercase()); .map(|encoding| encoding.to_lowercase());
match Body::new(self.raw_body(), &ctype, &transfer_encoding) { match Body::new(self.raw_body(), &ctype, &transfer_encoding) {
Body::Base64(body) | Body::QuotedPrintable(body) => Ok(body.get_decoded_as_string()?), Body::Base64(body) | Body::QuotedPrintable(body) => {
Body::SevenBit(body) | Body::EightBit(body) => Ok(body.get_as_string()?), Ok(body.get_decoded_as_string().map_err(parse_error)?)
Body::Binary(body) => Ok(body.get_as_string()?), }
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 /// # 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> { fn get_content_raw(&self) -> Result<Vec<u8>, CowMailError> {
let ctype = self.content_type(); let ctype = self.content_type();
let transfer_encoding = self let transfer_encoding = self
.find_value("Content-Transfer-Encoding") .find_value("Content-Transfer-Encoding")
.map(|encoding| encoding.to_lowercase()); .map(|encoding| encoding.to_lowercase());
match Body::new(self.raw_body(), &ctype, &transfer_encoding) { 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::SevenBit(body) | Body::EightBit(body) => Ok(Vec::from(body.get_raw())),
Body::Binary(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 /// # 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. /// a broken invariant of the bridge to mailparse and should not occur.
/// ///
/// # Examples /// # Examples
@@ -1350,7 +1384,7 @@ impl std::io::Read for CowMailReader<'_> {
/// assert_eq!(mail.find_value("Subject").unwrap(), "Hi"); /// 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> { 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) build_mail(raw_data, &parsed, false)
} }