own ContentType, drop the Deref to mailparse

This commit is contained in:
2026-10-10 22:39:53 +02:00
parent 37db780448
commit bed3dc8bad
2 changed files with 296 additions and 112 deletions
+235 -51
View File
@@ -17,8 +17,10 @@ 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, ParsedContentDisposition, ParsedContentType}; pub use mailparse::{DispositionType, ParsedContentDisposition};
use mailparse::{ParsedMail, body::Body, parse_content_disposition, parse_content_type}; use mailparse::{
ParsedContentType, 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
/// [`CowMail::set_content`] and [`CowMail::set_content_raw`], and [`names`] / [`values`] hold the /// [`CowMail::set_content`] and [`CowMail::set_content_raw`], and [`names`] / [`values`] hold the
@@ -28,7 +30,7 @@ pub use mediatype::{MediaType, names, values};
use mediatype::{MediaTypeBuf, Name, ReadParams, WriteParams, names::*, values::UTF_8}; use mediatype::{MediaTypeBuf, Name, ReadParams, WriteParams, names::*, values::UTF_8};
use rand::{RngExt, distr::Alphanumeric, rng}; use rand::{RngExt, distr::Alphanumeric, rng};
use std::borrow::Cow; use std::borrow::Cow;
use std::ops::Deref; use std::fmt::Display;
/// A failure reported by the parser the crate uses internally. /// A failure reported by the parser the crate uses internally.
/// ///
@@ -162,66 +164,248 @@ pub enum MessageType {
MultiPart(String), MultiPart(String),
} }
/// A parsed `Content-Type` header: the mimetype, the charset and the parameters. /// A `Content-Type`: the mimetype plus its parameters.
/// ///
/// A thin wrapper around [`ParsedContentType`] that it [`Deref`]s to, so `mimetype`, `charset` and /// The type both sides of the crate speak: [`Mail::content_type`] hands one back for a part, and
/// `params` are reachable directly. It exists to add [`Clone`] and a default that knows about /// [`CowMail::set_content`] takes one for the part it writes. [`Display`](std::fmt::Display)
/// `multipart/digest` (see [`default_conditional`](Self::default_conditional)). /// renders it as it belongs in the header, which is how it ends up serialized.
/// ///
/// Built from a header value with `ContentType::from("text/plain; charset=utf-8")`, or obtained /// Build one with [`new`](Self::new), take a ready-made one from [`mime`], or parse a header value
/// from a part with [`Mail::content_type`]. Note that the latter re-parses the header on every /// with [`parse`](Self::parse) (also `ContentType::from("text/plain; charset=utf-8")`). Note that
/// call, so hold on to the result rather than asking repeatedly. /// [`Mail::content_type`] re-parses the header on every call, so hold on to the result rather than
#[derive(Debug, Default)] /// asking repeatedly.
///
/// The `charset` is a parameter like any other — read it with [`charset`](Self::charset), which
/// falls back to `us-ascii` as RFC 2045 section 5.2 prescribes.
///
/// # Examples
///
/// ```
/// use cow_mail::ContentType;
///
/// let ctype = ContentType::parse("Text/HTML; Charset=\"utf-8\"");
/// assert_eq!(ctype.mimetype(), "text/html");
/// assert_eq!(ctype.subty(), "html");
/// assert_eq!(ctype.charset(), "utf-8");
///
/// // A parameter is quoted only when it has to be.
/// let built = ContentType::new("image", "png").with_param("name", "logo.png");
/// assert_eq!(built.to_string(), "image/png; name=logo.png");
/// let spaced = built.with_param("name", "my logo.png");
/// assert_eq!(spaced.to_string(), "image/png; name=\"my logo.png\"");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentType { pub struct ContentType {
ctype: ParsedContentType, /// Always lowercase, so comparisons can be plain string comparisons.
mimetype: Cow<'static, str>,
/// Keys lowercase, values unquoted and unescaped. A `Vec` rather than a map, to keep the
/// parameters in the order they were added — that order is what gets serialized.
params: Vec<(Cow<'static, str>, Cow<'static, str>)>,
} }
impl ContentType { impl ContentType {
/// A content type from a `&'static str` that is already lowercase, in a `const` context.
///
/// This is how the constants in [`mime`] are built. Nothing lowercases the string, so an
/// uppercase one would break the comparisons the rest of the crate does; use
/// [`new`](Self::new) or [`parse`](Self::parse) for anything that is not a literal.
pub const fn from_static(mimetype: &'static str) -> Self {
Self {
mimetype: Cow::Borrowed(mimetype),
params: Vec::new(),
}
}
/// A content type from a type and a subtype, e.g. `ContentType::new("image", "png")`.
///
/// Both are lowercased. For a type that [`mime`] already has a constant for, use that instead.
pub fn new(ty: &str, subty: &str) -> Self {
Self {
mimetype: Cow::Owned(format!(
"{}/{}",
ty.to_ascii_lowercase(),
subty.to_ascii_lowercase()
)),
params: Vec::new(),
}
}
/// Parse a `Content-Type` header value.
///
/// The mimetype and the parameter keys come back lowercased, parameter values unquoted. An
/// unparseable value yields the default, `text/plain`.
pub fn parse(value: &str) -> Self {
Self::from_parsed(parse_content_type(value))
}
/// The content type to assume for a part that has no `Content-Type` header. /// The content type to assume for a part that has no `Content-Type` header.
/// ///
/// `text/plain; charset=us-ascii` normally, but `message/rfc822` for a part of a /// `text/plain` normally, but `message/rfc822` for a part of a `multipart/digest`, per RFC
/// `multipart/digest`, per RFC 2046 section 5.1.5. Pass whether the part sits in such a digest. /// 2046 section 5.1.5. Pass whether the part sits in such a digest.
pub fn default_conditional(in_multipart_digest: bool) -> Self { pub fn default_conditional(in_multipart_digest: bool) -> Self {
let mut ctype = ParsedContentType::default(); match in_multipart_digest {
if in_multipart_digest { true => mime::MESSAGE_RFC822,
ctype.mimetype = "message/rfc822".to_string(); false => mime::TEXT_PLAIN,
} }
Self { ctype }
} }
}
impl Clone for ContentType { /// The full mimetype, lowercase, e.g. `text/plain`.
fn clone(&self) -> Self { pub fn mimetype(&self) -> &str {
&self.mimetype
}
/// The type half of the mimetype, e.g. `text` of `text/plain`.
pub fn ty(&self) -> &str {
match self.mimetype.split_once('/') {
Some((ty, _)) => ty,
None => &self.mimetype,
}
}
/// The subtype half of the mimetype, e.g. `plain` of `text/plain`.
///
/// Empty for a mimetype without a `/`, which only a malformed header can produce.
pub fn subty(&self) -> &str {
match self.mimetype.split_once('/') {
Some((_, subty)) => subty,
None => "",
}
}
/// The `charset` parameter, or `us-ascii` when there is none (RFC 2045 section 5.2).
pub fn charset(&self) -> &str {
self.get_param("charset").unwrap_or("us-ascii")
}
/// The parameters, in the order they will be serialized in.
pub fn params(&self) -> impl Iterator<Item = (&str, &str)> {
self.params.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))
}
/// The parameter `key`, matched case-insensitively.
pub fn get_param(&self, key: &str) -> Option<&str> {
self.params
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, v)| v.as_ref())
}
/// Set the parameter `key` to `value`, replacing it if it is already there.
///
/// The key is lowercased; the value is kept as given and quoted on serialization if it needs
/// to be. A replaced parameter keeps its position.
pub fn set_param(&mut self, key: &str, value: impl Into<String>) {
let value = Cow::Owned(value.into());
match self.params.iter_mut().find(|(k, _)| k.eq_ignore_ascii_case(key)) {
Some(param) => param.1 = value,
None => self.params.push((Cow::Owned(key.to_ascii_lowercase()), value)),
}
}
/// [`set_param`](Self::set_param) as a builder step.
pub fn with_param(mut self, key: &str, value: impl Into<String>) -> Self {
self.set_param(key, value);
self
}
/// Build from what the parser produced.
///
/// `charset` is dropped: the parser reports it both as a field and as a parameter, defaulting
/// the field to `us-ascii`, and keeping both would serialize it twice.
pub(crate) fn from_parsed(ctype: ParsedContentType) -> Self {
Self { Self {
ctype: ParsedContentType { mimetype: Cow::Owned(ctype.mimetype),
mimetype: self.ctype.mimetype.clone(), params: ctype
charset: self.ctype.charset.clone(), .params
params: self.ctype.params.clone(), .into_iter()
}, .map(|(k, v)| (Cow::Owned(k), Cow::Owned(v)))
.collect(),
}
}
/// Hand back to the parser, for the body decoding it does.
pub(crate) fn to_parsed(&self) -> ParsedContentType {
ParsedContentType {
mimetype: self.mimetype.to_string(),
charset: self.charset().to_string(),
params: self
.params
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
} }
} }
} }
impl Deref for ContentType { impl Default for ContentType {
type Target = ParsedContentType; fn default() -> Self {
mime::TEXT_PLAIN
fn deref(&self) -> &Self::Target {
&self.ctype
} }
} }
impl From<ParsedContentType> for ContentType { impl Display for ContentType {
fn from(ctype: ParsedContentType) -> Self { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Self { ctype } write!(f, "{}", self.mimetype)?;
for (key, value) in &self.params {
match is_token(value) {
true => write!(f, "; {key}={value}")?,
false => {
let escaped = value.replace('\\', r"\\").replace('"', "\\\"");
write!(f, "; {key}=\"{escaped}\"")?
}
}
}
Ok(())
} }
} }
impl From<&str> for ContentType { impl From<&str> for ContentType {
fn from(value: &str) -> Self { fn from(value: &str) -> Self {
parse_content_type(value).into() Self::parse(value)
} }
} }
/// Whether `value` is an RFC 2045 token, i.e. can go into a header unquoted.
fn is_token(value: &str) -> bool {
!value.is_empty()
&& value
.bytes()
.all(|b| b > 0x20 && b < 0x7f && !br#"()<>@,;:\"/[]?="#.contains(&b))
}
/// Ready-made [`ContentType`] constants for the types this crate itself deals in.
///
/// Anything else is a [`ContentType::new`] or a [`ContentType::parse`] away — this is not meant to
/// grow into a mimetype registry.
///
/// # Examples
///
/// ```
/// use cow_mail::mime;
///
/// assert_eq!(mime::TEXT_HTML.mimetype(), "text/html");
/// assert_eq!(mime::MULTIPART_MIXED.subty(), "mixed");
/// ```
pub mod mime {
use super::ContentType;
/// `text/plain`, the default content type of a part without a `Content-Type` header.
pub const TEXT_PLAIN: ContentType = ContentType::from_static("text/plain");
/// `text/html`.
pub const TEXT_HTML: ContentType = ContentType::from_static("text/html");
/// `message/rfc822`, the default content type inside a `multipart/digest`.
pub const MESSAGE_RFC822: ContentType = ContentType::from_static("message/rfc822");
/// `multipart/mixed`, a part and its attachments.
pub const MULTIPART_MIXED: ContentType = ContentType::from_static("multipart/mixed");
/// `multipart/alternative`, the same content in two representations.
pub const MULTIPART_ALTERNATIVE: ContentType =
ContentType::from_static("multipart/alternative");
/// `multipart/related`, a part and the resources it refers to.
pub const MULTIPART_RELATED: ContentType = ContentType::from_static("multipart/related");
/// `multipart/digest`, a collection of messages.
pub const MULTIPART_DIGEST: ContentType = ContentType::from_static("multipart/digest");
}
/// The MIME view of a message or one of its parts, on top of its headers. /// The MIME view of a message or one of its parts, on top of its headers.
/// ///
/// Implementors supply storage — the body bytes, the default content type, and the two ways of /// Implementors supply storage — the body bytes, the default content type, and the two ways of
@@ -280,13 +464,13 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
/// [`SinglePart`](MessageType::SinglePart), as does an unparseable content type. /// [`SinglePart`](MessageType::SinglePart), as does an unparseable content type.
fn message_type(&self) -> MessageType { fn message_type(&self) -> MessageType {
let ctype = self.content_type(); let ctype = self.content_type();
match ctype.mimetype.parse::<MediaTypeBuf>() { match ctype.mimetype().parse::<MediaTypeBuf>() {
Ok(mtype) => { Ok(mtype) => {
let mut res = MessageType::SinglePart; let mut res = MessageType::SinglePart;
if mtype.ty() == MULTIPART if mtype.ty() == MULTIPART
&& let Some(boundary) = ctype.params.get("boundary") && let Some(boundary) = ctype.get_param("boundary")
{ {
res = MessageType::MultiPart(boundary.clone()); res = MessageType::MultiPart(boundary.to_string());
} }
res res
} }
@@ -358,7 +542,7 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
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.to_parsed(), &transfer_encoding) {
Body::Base64(body) | Body::QuotedPrintable(body) => { Body::Base64(body) | Body::QuotedPrintable(body) => {
Ok(body.get_decoded_as_string().map_err(parse_error)?) Ok(body.get_decoded_as_string().map_err(parse_error)?)
} }
@@ -380,7 +564,7 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
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.to_parsed(), &transfer_encoding) {
Body::Base64(body) | Body::QuotedPrintable(body) => Ok(body.get_decoded().map_err(parse_error)?), 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())),
@@ -415,13 +599,13 @@ impl PartInfo {
self.media.subty() self.media.subty()
} }
fn boundary(&self) -> Option<&String> { fn boundary(&self) -> Option<&str> {
self.ctype.params.get("boundary") self.ctype.get_param("boundary")
} }
/// The `start` parameter of a multipart/related, naming its root part. /// The `start` parameter of a multipart/related, naming its root part.
fn start(&self) -> Option<&String> { fn start(&self) -> Option<&str> {
self.ctype.params.get("start") self.ctype.get_param("start")
} }
fn is_multipart(&self) -> bool { fn is_multipart(&self) -> bool {
@@ -608,7 +792,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
fn part_info(&self) -> PartInfo { fn part_info(&self) -> PartInfo {
let ctype = self.content_type(); let ctype = self.content_type();
let media = ctype let media = ctype
.mimetype .mimetype()
.parse::<MediaTypeBuf>() .parse::<MediaTypeBuf>()
.unwrap_or_else(|_| MediaTypeBuf::new(TEXT, PLAIN)); .unwrap_or_else(|_| MediaTypeBuf::new(TEXT, PLAIN));
PartInfo { ctype, media } PartInfo { ctype, media }
@@ -629,7 +813,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
/// let mut part = CowMail::default(); /// let mut part = CowMail::default();
/// part.set_content("<p>hi</p>", Some(MediaType::new(names::TEXT, names::HTML))); /// part.set_content("<p>hi</p>", Some(MediaType::new(names::TEXT, names::HTML)));
/// ///
/// assert_eq!(part.content_type().mimetype, "text/html"); /// assert_eq!(part.content_type().mimetype(), "text/html");
/// assert_eq!(part.get_content().unwrap(), "<p>hi</p>"); /// assert_eq!(part.get_content().unwrap(), "<p>hi</p>");
/// ///
/// // Non-ASCII text gets a charset, and an encoding that survives transport. /// // Non-ASCII text gets a charset, and an encoding that survives transport.
@@ -677,7 +861,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
pub fn get_filename(&self) -> Option<String> { pub fn get_filename(&self) -> Option<String> {
self.content_disposition() self.content_disposition()
.and_then(|disp| disp.params.get("filename").cloned()) .and_then(|disp| disp.params.get("filename").cloned())
.or_else(|| self.content_type().params.get("name").cloned()) .or_else(|| self.content_type().get_param("name").map(str::to_string))
} }
/// The `boundary` parameter of this part's `Content-Type` header, if it has one. /// The `boundary` parameter of this part's `Content-Type` header, if it has one.
@@ -896,7 +1080,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
/// mail.make_alternative(Some("boundary".to_string())); /// mail.make_alternative(Some("boundary".to_string()));
/// ///
/// // The content moved down one level; Subject stayed up top. /// // The content moved down one level; Subject stayed up top.
/// assert_eq!(mail.content_type().mimetype, "multipart/alternative"); /// assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
/// assert_eq!(mail.find_value("Subject").unwrap(), "Hi"); /// assert_eq!(mail.find_value("Subject").unwrap(), "Hi");
/// assert_eq!(mail.subparts[0].get_content().unwrap(), "plain"); /// assert_eq!(mail.subparts[0].get_content().unwrap(), "plain");
/// ///
@@ -963,7 +1147,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
/// ); /// );
/// mail.add_attachment(attachment); /// mail.add_attachment(attachment);
/// ///
/// assert_eq!(mail.content_type().mimetype, "multipart/mixed"); /// assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
/// let last = mail.subparts.last().unwrap(); /// let last = mail.subparts.last().unwrap();
/// assert!(last.is_attachment()); /// assert!(last.is_attachment());
/// assert_eq!(last.find_value("Content-Transfer-Encoding").unwrap(), "base64"); /// assert_eq!(last.find_value("Content-Transfer-Encoding").unwrap(), "base64");
@@ -1127,12 +1311,12 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
/// mail.add_header("Subject", "Hi"); /// mail.add_header("Subject", "Hi");
/// mail.set_body("plain", "<p>html</p>").unwrap(); /// mail.set_body("plain", "<p>html</p>").unwrap();
/// ///
/// assert_eq!(mail.content_type().mimetype, "multipart/alternative"); /// assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
/// assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "plain"); /// assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "plain");
/// assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>html</p>"); /// assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>html</p>");
/// ///
/// // Plain comes first. /// // Plain comes first.
/// assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); /// assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
/// ``` /// ```
pub fn set_body<T: ToString>(&mut self, text: T, html: T) -> Result<(), CowMailError> { pub fn set_body<T: ToString>(&mut self, text: T, html: T) -> Result<(), CowMailError> {
self.set_text_body(text)?; self.set_text_body(text)?;
+61 -61
View File
@@ -74,8 +74,8 @@
CowMail::try_from(concat!("Content-Type: text/plain\n", "\n", "This is a test message").as_bytes()) CowMail::try_from(concat!("Content-Type: text/plain\n", "\n", "This is a test message").as_bytes())
.unwrap(); .unwrap();
let ctype = mail.content_type(); let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "text/plain"); assert_eq!(ctype.mimetype(), "text/plain");
assert_eq!(ctype.charset, "us-ascii"); assert_eq!(ctype.charset(), "us-ascii");
assert!(!mail.is_multipart()); assert!(!mail.is_multipart());
assert_eq!(mail.get_content().unwrap(), "This is a test message"); assert_eq!(mail.get_content().unwrap(), "This is a test message");
@@ -84,15 +84,15 @@
assert_eq!(mail.get_content().unwrap(), ""); assert_eq!(mail.get_content().unwrap(), "");
mail.set_content("This is a new test message ❤\n".to_string(), None); mail.set_content("This is a new test message ❤\n".to_string(), None);
let ctype = mail.content_type(); let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "text/plain"); assert_eq!(ctype.mimetype(), "text/plain");
assert_eq!(ctype.charset, "UTF-8"); assert_eq!(ctype.charset(), "UTF-8");
mail.clear_content(); mail.clear_content();
assert_eq!(mail.get_content().unwrap(), ""); assert_eq!(mail.get_content().unwrap(), "");
mail.set_content("This is a new test message\n".to_string(), None); mail.set_content("This is a new test message\n".to_string(), None);
let ctype = mail.content_type(); let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "text/plain"); assert_eq!(ctype.mimetype(), "text/plain");
assert_eq!(ctype.charset, "us-ascii"); assert_eq!(ctype.charset(), "us-ascii");
mail.add_header("From", "\"John Doe\" <john.doe@example.com>"); mail.add_header("From", "\"John Doe\" <john.doe@example.com>");
@@ -106,8 +106,8 @@
let alt_boundary = mail.get_boundary().unwrap(); let alt_boundary = mail.get_boundary().unwrap();
let ctype = mail.content_type(); let ctype = mail.content_type();
assert_eq!(ctype.mimetype, "multipart/alternative"); assert_eq!(ctype.mimetype(), "multipart/alternative");
assert_eq!(ctype.params.get("boundary").unwrap(), &alt_boundary); assert_eq!(ctype.get_param("boundary").unwrap(), alt_boundary);
assert_eq!( assert_eq!(
mail.find_header("Content-Type").unwrap().get_value(), mail.find_header("Content-Type").unwrap().get_value(),
format!("multipart/alternative; boundary=\"{}\"", alt_boundary) format!("multipart/alternative; boundary=\"{}\"", alt_boundary)
@@ -187,10 +187,10 @@
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/alternative"); assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[1].content_type().mimetype(), "text/html");
assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "Plain text"); assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "Plain text");
assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>HTML</p>"); assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>HTML</p>");
} }
@@ -205,10 +205,10 @@
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/alternative"); assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[1].content_type().mimetype(), "text/html");
assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "Plain text"); assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "Plain text");
assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>HTML</p>"); assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>HTML</p>");
} }
@@ -219,15 +219,15 @@
let mut mail = CowMail::<CowHeader>::new(); let mut mail = CowMail::<CowHeader>::new();
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
assert_eq!(mail.content_type().mimetype, "text/plain"); assert_eq!(mail.content_type().mimetype(), "text/plain");
assert!(!mail.is_multipart()); assert!(!mail.is_multipart());
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/alternative"); assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[1].content_type().mimetype(), "text/html");
} }
// Test: empty + set_html + set_text -> alternative[plain, html] // Test: empty + set_html + set_text -> alternative[plain, html]
@@ -236,15 +236,15 @@
let mut mail = CowMail::<CowHeader>::new(); let mut mail = CowMail::<CowHeader>::new();
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/alternative"); assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
assert!(mail.is_multipart()); assert!(mail.is_multipart());
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/alternative"); assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[1].content_type().mimetype(), "text/html");
} }
// Test: mixed[attachment] + set_text -> mixed[plain, attachment] // Test: mixed[attachment] + set_text -> mixed[plain, attachment]
@@ -266,14 +266,14 @@
) )
.unwrap(); .unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/mixed"); assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
assert_eq!(mail.subparts.len(), 1); assert_eq!(mail.subparts.len(), 1);
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/mixed"); assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
assert!(mail.subparts[1].is_attachment()); assert!(mail.subparts[1].is_attachment());
} }
@@ -298,12 +298,12 @@
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/mixed"); assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "multipart/alternative"); assert_eq!(mail.subparts[0].content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts[0].subparts.len(), 2); assert_eq!(mail.subparts[0].subparts.len(), 2);
assert_eq!(mail.subparts[0].subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[0].subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[0].subparts[1].content_type().mimetype(), "text/html");
assert!(mail.subparts[1].is_attachment()); assert!(mail.subparts[1].is_attachment());
} }
@@ -332,12 +332,12 @@
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/mixed"); assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "multipart/alternative"); assert_eq!(mail.subparts[0].content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts[0].subparts.len(), 2); assert_eq!(mail.subparts[0].subparts.len(), 2);
assert_eq!(mail.subparts[0].subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[0].subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[0].subparts[1].content_type().mimetype(), "text/html");
assert!(mail.subparts[1].is_attachment()); assert!(mail.subparts[1].is_attachment());
} }
@@ -363,13 +363,13 @@
) )
.unwrap(); .unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/related"); assert_eq!(mail.content_type().mimetype(), "multipart/related");
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/alternative"); assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[1].content_type().mimetype, "multipart/related"); assert_eq!(mail.subparts[1].content_type().mimetype(), "multipart/related");
assert_eq!(mail.subparts[1].subparts.len(), 2); assert_eq!(mail.subparts[1].subparts.len(), 2);
} }
@@ -419,10 +419,10 @@
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
// the related container is wrapped in an alternative holding both bodies // the related container is wrapped in an alternative holding both bodies
assert_eq!(mail.content_type().mimetype, "multipart/alternative"); assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "multipart/related"); assert_eq!(mail.subparts[0].content_type().mimetype(), "multipart/related");
assert_eq!(mail.subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[1].content_type().mimetype(), "text/html");
assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "Plain text\n"); assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "Plain text\n");
assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>HTML</p>"); assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>HTML</p>");
} }
@@ -460,11 +460,11 @@
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/mixed"); assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "multipart/alternative"); assert_eq!(mail.subparts[0].content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts[0].subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[0].subparts[1].content_type().mimetype, "multipart/related"); assert_eq!(mail.subparts[0].subparts[1].content_type().mimetype(), "multipart/related");
assert!(mail.subparts[1].is_attachment()); assert!(mail.subparts[1].is_attachment());
} }
@@ -487,10 +487,10 @@
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/alternative"); assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[1].content_type().mimetype(), "text/html");
} }
// Test: alternative[html] + set_text -> alternative[plain, html] // Test: alternative[html] + set_text -> alternative[plain, html]
@@ -512,10 +512,10 @@
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/alternative"); assert_eq!(mail.content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[1].content_type().mimetype(), "text/html");
} }
// Test: update existing body in alternative[plain, html] // Test: update existing body in alternative[plain, html]
@@ -572,9 +572,9 @@
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/mixed"); assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[0].get_content().unwrap(), "Plain text"); assert_eq!(mail.subparts[0].get_content().unwrap(), "Plain text");
assert!(mail.subparts[1].is_attachment()); assert!(mail.subparts[1].is_attachment());
assert_eq!(mail.subparts[1].get_content().unwrap(), "binary data"); assert_eq!(mail.subparts[1].get_content().unwrap(), "binary data");
@@ -599,9 +599,9 @@
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/mixed"); assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[0].content_type().mimetype(), "text/html");
assert!(mail.subparts[1].is_attachment()); assert!(mail.subparts[1].is_attachment());
} }
@@ -622,12 +622,12 @@
mail.set_text_body("Plain text").unwrap(); mail.set_text_body("Plain text").unwrap();
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/mixed"); assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
assert_eq!(mail.subparts.len(), 2); assert_eq!(mail.subparts.len(), 2);
assert_eq!(mail.subparts[0].content_type().mimetype, "multipart/alternative"); assert_eq!(mail.subparts[0].content_type().mimetype(), "multipart/alternative");
assert_eq!(mail.subparts[0].subparts.len(), 2); assert_eq!(mail.subparts[0].subparts.len(), 2);
assert_eq!(mail.subparts[0].subparts[0].content_type().mimetype, "text/plain"); assert_eq!(mail.subparts[0].subparts[0].content_type().mimetype(), "text/plain");
assert_eq!(mail.subparts[0].subparts[1].content_type().mimetype, "text/html"); assert_eq!(mail.subparts[0].subparts[1].content_type().mimetype(), "text/html");
assert!(mail.subparts[1].is_attachment()); assert!(mail.subparts[1].is_attachment());
assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "Plain text"); assert_eq!(mail.get_body_part(BodyType::PLAIN).unwrap().get_content().unwrap(), "Plain text");
assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>HTML</p>"); assert_eq!(mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(), "<p>HTML</p>");
@@ -662,10 +662,10 @@
mail.set_html_body("<p>HTML</p>").unwrap(); mail.set_html_body("<p>HTML</p>").unwrap();
assert_eq!(mail.content_type().mimetype, "multipart/mixed"); assert_eq!(mail.content_type().mimetype(), "multipart/mixed");
assert_eq!(mail.subparts.len(), 3); assert_eq!(mail.subparts.len(), 3);
assert!(mail.subparts[0].is_attachment()); assert!(mail.subparts[0].is_attachment());
assert_eq!(mail.subparts[1].content_type().mimetype, "multipart/alternative"); assert_eq!(mail.subparts[1].content_type().mimetype(), "multipart/alternative");
assert!(mail.subparts[2].is_attachment()); assert!(mail.subparts[2].is_attachment());
} }
@@ -754,7 +754,7 @@
let mail = CowMail::try_from(raw.as_bytes()).unwrap(); let mail = CowMail::try_from(raw.as_bytes()).unwrap();
assert_eq!(mail.subparts.len(), 1); assert_eq!(mail.subparts.len(), 1);
assert_eq!(mail.subparts[0].content_type().mimetype, "message/rfc822"); assert_eq!(mail.subparts[0].content_type().mimetype(), "message/rfc822");
} }
// Test: a nested multipart with a preamble and an epilogue round-trips // Test: a nested multipart with a preamble and an epilogue round-trips