drop mediatype, match mimetypes as lowercase strings
This commit is contained in:
@@ -18,5 +18,4 @@ email-encoding = "*"
|
|||||||
quoted_printable = "*"
|
quoted_printable = "*"
|
||||||
charset = "*"
|
charset = "*"
|
||||||
mailparse = "*"
|
mailparse = "*"
|
||||||
mediatype = "*"
|
|
||||||
rand = "*"
|
rand = "*"
|
||||||
|
|||||||
+43
-71
@@ -18,7 +18,6 @@ use mailparse::{
|
|||||||
ParsedContentDisposition, ParsedContentType, ParsedMail, body::Body, parse_content_disposition, parse_content_type,
|
ParsedContentDisposition, ParsedContentType, ParsedMail, body::Body, parse_content_disposition, parse_content_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mediatype::{MediaTypeBuf, Name, names::*};
|
|
||||||
use rand::{RngExt, distr::Alphanumeric, rng};
|
use rand::{RngExt, distr::Alphanumeric, rng};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
@@ -111,6 +110,17 @@ impl std::fmt::Display for CowMailError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The mimetype halves the crate matches against. Every mimetype the crate holds is lowercase —
|
||||||
|
// `ContentType::mimetype` guarantees it — so matching one is a plain string comparison. Not to be
|
||||||
|
// confused with the `mime` constants, which are whole `ContentType`s.
|
||||||
|
const TEXT: &str = "text";
|
||||||
|
const MULTIPART: &str = "multipart";
|
||||||
|
const PLAIN: &str = "plain";
|
||||||
|
const HTML: &str = "html";
|
||||||
|
const MIXED: &str = "mixed";
|
||||||
|
const ALTERNATIVE: &str = "alternative";
|
||||||
|
const RELATED: &str = "related";
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -133,7 +143,7 @@ impl BodyType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The subtype a part of this body type declares.
|
/// The subtype a part of this body type declares.
|
||||||
fn name(self) -> Name<'static> {
|
fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::PLAIN => PLAIN,
|
Self::PLAIN => PLAIN,
|
||||||
Self::HTML => HTML,
|
Self::HTML => HTML,
|
||||||
@@ -531,21 +541,12 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
|
|||||||
|
|
||||||
/// Whether the part carries subparts, and the boundary separating them.
|
/// Whether the part carries subparts, and the boundary separating them.
|
||||||
///
|
///
|
||||||
/// `multipart/*` without a `boundary` parameter counts as
|
/// A content type that is not `multipart/*`, or one without a `boundary` parameter, counts as
|
||||||
/// [`SinglePart`](MessageType::SinglePart), as does an unparseable content type.
|
/// [`SinglePart`](MessageType::SinglePart).
|
||||||
fn message_type(&self) -> MessageType {
|
fn message_type(&self) -> MessageType {
|
||||||
let ctype = self.content_type();
|
match multipart_boundary(&self.content_type()) {
|
||||||
match ctype.mimetype().parse::<MediaTypeBuf>() {
|
Some(boundary) => MessageType::MultiPart(boundary.to_string()),
|
||||||
Ok(mtype) => {
|
None => MessageType::SinglePart,
|
||||||
let mut res = MessageType::SinglePart;
|
|
||||||
if mtype.ty() == MULTIPART
|
|
||||||
&& let Some(boundary) = ctype.get_param("boundary")
|
|
||||||
{
|
|
||||||
res = MessageType::MultiPart(boundary.to_string());
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
Err(_) => MessageType::SinglePart,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,32 +656,14 @@ pub fn generate_boundary() -> String {
|
|||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The parsed Content-Type of a part, used to prevent excessive calls go get_mimetype during traversal.
|
/// The boundary of a part that declares subparts: `multipart/*` carrying a `boundary` parameter.
|
||||||
struct PartInfo {
|
///
|
||||||
ctype: ContentType,
|
/// The single place that rule is written. It is the same one mailparse applies when it decides
|
||||||
media: MediaTypeBuf,
|
/// where the parts are, so the two cannot disagree.
|
||||||
}
|
fn multipart_boundary(ctype: &ContentType) -> Option<&str> {
|
||||||
|
match ctype.ty() == MULTIPART {
|
||||||
impl PartInfo {
|
true => ctype.get_param("boundary"),
|
||||||
fn ty(&self) -> Name<'_> {
|
false => None,
|
||||||
self.media.ty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subty(&self) -> Name<'_> {
|
|
||||||
self.media.subty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn boundary(&self) -> Option<&str> {
|
|
||||||
self.ctype.get_param("boundary")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `start` parameter of a multipart/related, naming its root part.
|
|
||||||
fn start(&self) -> Option<&str> {
|
|
||||||
self.ctype.get_param("start")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_multipart(&self) -> bool {
|
|
||||||
self.ty() == MULTIPART && self.boundary().is_some()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,15 +843,6 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn part_info(&self) -> PartInfo {
|
|
||||||
let ctype = self.content_type();
|
|
||||||
let media = ctype
|
|
||||||
.mimetype()
|
|
||||||
.parse::<MediaTypeBuf>()
|
|
||||||
.unwrap_or_else(|_| MediaTypeBuf::new(TEXT, PLAIN));
|
|
||||||
PartInfo { ctype, media }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the content of this part with text.
|
/// Replace the content of this part with text.
|
||||||
///
|
///
|
||||||
/// Drops the existing content first — all `Content-*` headers, the body and the subparts, see
|
/// Drops the existing content first — all `Content-*` headers, the body and the subparts, see
|
||||||
@@ -964,16 +938,16 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this MIME part is a text body (plain/html), not a body, or needs traversal.
|
/// Check if this MIME part is a text body (plain/html), not a body, or needs traversal.
|
||||||
fn check_body_status(&self, info: &PartInfo, bodytype: Option<BodyType>) -> BodyCheckStatus {
|
fn check_body_status(&self, ctype: &ContentType, bodytype: Option<BodyType>) -> BodyCheckStatus {
|
||||||
match info.is_multipart() {
|
match multipart_boundary(ctype).is_some() {
|
||||||
true if self.subparts.is_empty() => BodyCheckStatus::NotBody,
|
true if self.subparts.is_empty() => BodyCheckStatus::NotBody,
|
||||||
true => BodyCheckStatus::NeedsTraversal,
|
true => BodyCheckStatus::NeedsTraversal,
|
||||||
false => {
|
false => {
|
||||||
if info.ty() != TEXT || self.is_attachment() {
|
if ctype.ty() != TEXT || self.is_attachment() {
|
||||||
return BodyCheckStatus::NotBody;
|
return BodyCheckStatus::NotBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtype = info.subty();
|
let subtype = ctype.subty();
|
||||||
let is_body = match bodytype {
|
let is_body = match bodytype {
|
||||||
Some(bodytype) => subtype == bodytype.name(),
|
Some(bodytype) => subtype == bodytype.name(),
|
||||||
None => subtype == PLAIN || subtype == HTML,
|
None => subtype == PLAIN || subtype == HTML,
|
||||||
@@ -987,8 +961,9 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_related_candidate(&self, info: &PartInfo) -> Option<usize> {
|
/// The subpart a `multipart/related` names with its `start` parameter, by `Content-ID`.
|
||||||
info.start().and_then(|start| {
|
fn get_related_candidate(&self, ctype: &ContentType) -> Option<usize> {
|
||||||
|
ctype.get_param("start").and_then(|start| {
|
||||||
self.subparts.iter().position(|part| {
|
self.subparts.iter().position(|part| {
|
||||||
part.headers
|
part.headers
|
||||||
.find_value("Content-ID")
|
.find_value("Content-ID")
|
||||||
@@ -1006,15 +981,15 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push_body_path(&self, bodytype: BodyType, path: &mut Vec<usize>) -> bool {
|
fn push_body_path(&self, bodytype: BodyType, path: &mut Vec<usize>) -> bool {
|
||||||
let info = self.part_info();
|
let ctype = self.content_type();
|
||||||
match self.check_body_status(&info, Some(bodytype)) {
|
match self.check_body_status(&ctype, Some(bodytype)) {
|
||||||
BodyCheckStatus::IsBody => true,
|
BodyCheckStatus::IsBody => true,
|
||||||
BodyCheckStatus::NotBody => false,
|
BodyCheckStatus::NotBody => false,
|
||||||
BodyCheckStatus::NeedsTraversal => {
|
BodyCheckStatus::NeedsTraversal => {
|
||||||
// A multipart/related names its root part with the `start` parameter.
|
// A multipart/related names its root part with the `start` parameter.
|
||||||
// When it resolves, that part is the only candidate.
|
// When it resolves, that part is the only candidate.
|
||||||
if info.subty() == RELATED
|
if ctype.subty() == RELATED
|
||||||
&& let Some(candidate) = self.get_related_candidate(&info)
|
&& let Some(candidate) = self.get_related_candidate(&ctype)
|
||||||
{
|
{
|
||||||
path.push(candidate);
|
path.push(candidate);
|
||||||
if self.subparts[candidate].push_body_path(bodytype, path) {
|
if self.subparts[candidate].push_body_path(bodytype, path) {
|
||||||
@@ -1080,9 +1055,9 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
Some(self.get_part_at_path_mut(&path))
|
Some(self.get_part_at_path_mut(&path))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_multipart(&mut self, to_type: Name, boundary: Option<String>) {
|
fn make_multipart(&mut self, to_type: &str, boundary: Option<String>) {
|
||||||
let info = self.part_info();
|
let ctype = self.content_type();
|
||||||
if info.ty() == MULTIPART && info.subty() == to_type {
|
if ctype.ty() == MULTIPART && ctype.subty() == to_type {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1172,7 +1147,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
|
|
||||||
/// `make_multipart` already returns early when this part is that multipart
|
/// `make_multipart` already returns early when this part is that multipart
|
||||||
/// subtype, so it covers both wrapping and appending to an existing container.
|
/// subtype, so it covers both wrapping and appending to an existing container.
|
||||||
fn add_multipart(&mut self, subpart: CowMail<'a, H>, subtype: Name) {
|
fn add_multipart(&mut self, subpart: CowMail<'a, H>, subtype: &str) {
|
||||||
self.make_multipart(subtype, None);
|
self.make_multipart(subtype, None);
|
||||||
self.subparts.push(subpart);
|
self.subparts.push(subpart);
|
||||||
}
|
}
|
||||||
@@ -1267,10 +1242,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
self.make_mixed(None);
|
self.make_mixed(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = self.part_info();
|
if self.content_type().subty() == RELATED && body_type == BodyType::PLAIN {
|
||||||
let subtype = info.subty();
|
|
||||||
|
|
||||||
if subtype == RELATED && body_type == BodyType::PLAIN {
|
|
||||||
self.make_alternative(None);
|
self.make_alternative(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1297,8 +1269,8 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let container = self.get_part_at_path_mut(container_path);
|
let container = self.get_part_at_path_mut(container_path);
|
||||||
let info = container.part_info();
|
let container_ctype = container.content_type();
|
||||||
let subtype = info.subty();
|
let subtype = container_ctype.subty();
|
||||||
|
|
||||||
if subtype == ALTERNATIVE {
|
if subtype == ALTERNATIVE {
|
||||||
container.insert_new_part(new_part, body_type);
|
container.insert_new_part(new_part, body_type);
|
||||||
|
|||||||
@@ -1117,3 +1117,101 @@ fn test_roundtrip_header_colon_only() {
|
|||||||
assert_eq!(mail.headers[0].get_value_raw(), b"");
|
assert_eq!(mail.headers[0].get_value_raw(), b"");
|
||||||
assert_eq!(mail.headers[0].get_raw().as_ref(), b":\r\n");
|
assert_eq!(mail.headers[0].get_raw().as_ref(), b":\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The crate matches mimetype halves as plain lowercase strings, with no
|
||||||
|
// RFC 5322 name validation in between. A malformed mimetype is therefore taken
|
||||||
|
// at face value instead of falling back to text/plain, so none of these parts
|
||||||
|
// is a body.
|
||||||
|
#[test]
|
||||||
|
fn test_malformed_mimetype_is_not_a_body() {
|
||||||
|
for raw in [
|
||||||
|
"Content-Type: text/\r\n\r\nbody\r\n",
|
||||||
|
"Content-Type: foo bar/baz\r\n\r\nbody\r\n",
|
||||||
|
"Content-Type: text\r\n\r\nbody\r\n",
|
||||||
|
] {
|
||||||
|
assert_roundtrip(raw);
|
||||||
|
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
assert!(mail.get_body_part(BodyType::PLAIN).is_none(), "{raw}");
|
||||||
|
assert!(mail.get_body_part(BodyType::HTML).is_none(), "{raw}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A structured-syntax suffix is part of the subtype, not stripped from it, so
|
||||||
|
// text/html+foo is not the text/html body.
|
||||||
|
#[test]
|
||||||
|
fn test_mimetype_suffix_is_part_of_the_subtype() {
|
||||||
|
let raw = "Content-Type: text/html+foo\r\n\r\n<p>HTML</p>\r\n";
|
||||||
|
assert_roundtrip(raw);
|
||||||
|
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
assert_eq!(mail.content_type().subty(), "html+foo");
|
||||||
|
assert!(mail.get_body_part(BodyType::HTML).is_none());
|
||||||
|
assert!(mail.get_body_part(BodyType::PLAIN).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// `multipart` without a subtype still splits into subparts, so is_multipart()
|
||||||
|
// has to agree with the parts that are actually there.
|
||||||
|
#[test]
|
||||||
|
fn test_multipart_without_subtype() {
|
||||||
|
let raw = concat!(
|
||||||
|
"Content-Type: multipart; boundary=b\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"--b\r\n",
|
||||||
|
"Content-Type: text/plain\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"plain\r\n",
|
||||||
|
"--b\r\n",
|
||||||
|
"Content-Type: text/html\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"<p>HTML</p>\r\n",
|
||||||
|
"--b--\r\n",
|
||||||
|
);
|
||||||
|
assert_roundtrip(raw);
|
||||||
|
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
assert!(mail.is_multipart());
|
||||||
|
assert_eq!(mail.subparts.len(), 2);
|
||||||
|
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>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mimetype is lowercased on the way in, which is what makes the plain
|
||||||
|
// string comparisons case-insensitive.
|
||||||
|
#[test]
|
||||||
|
fn test_mimetype_matching_is_case_insensitive() {
|
||||||
|
let raw = "Content-Type: TEXT/HTML\r\n\r\n<p>HTML</p>\r\n";
|
||||||
|
assert_roundtrip(raw);
|
||||||
|
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
assert_eq!(mail.content_type().mimetype(), "text/html");
|
||||||
|
assert_eq!(
|
||||||
|
mail.get_body_part(BodyType::HTML).unwrap().get_content().unwrap(),
|
||||||
|
"<p>HTML</p>"
|
||||||
|
);
|
||||||
|
assert!(mail.get_body_part(BodyType::PLAIN).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// MULTIPART/MIXED declares a container just as multipart/mixed does, and
|
||||||
|
// make_mixed must see it as one rather than nesting another level.
|
||||||
|
#[test]
|
||||||
|
fn test_make_mixed_is_case_insensitive() {
|
||||||
|
let raw = concat!(
|
||||||
|
"Content-Type: MULTIPART/MIXED; boundary=b\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"--b\r\n",
|
||||||
|
"Content-Type: text/plain\r\n",
|
||||||
|
"\r\n",
|
||||||
|
"plain\r\n",
|
||||||
|
"--b--\r\n",
|
||||||
|
);
|
||||||
|
let mut mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
assert!(mail.is_multipart());
|
||||||
|
|
||||||
|
mail.make_mixed(None);
|
||||||
|
assert_eq!(mail.subparts.len(), 1);
|
||||||
|
assert_eq!(String::from_utf8_lossy(&serialize(&mail)), raw);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user