some refactoring and content transfer encoding is case insensitive
This commit is contained in:
+39
-56
@@ -18,7 +18,6 @@ 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::{distr::Alphanumeric, rng, RngExt};
|
use rand::{distr::Alphanumeric, rng, RngExt};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::boxed::Box;
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -177,7 +176,9 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
|
|||||||
|
|
||||||
fn get_content(&self) -> Result<String, CowMailError> {
|
fn get_content(&self) -> Result<String, CowMailError> {
|
||||||
let ctype = self.content_type();
|
let ctype = self.content_type();
|
||||||
let transfer_encoding = self.find_value("Content-Transfer-Encoding");
|
let transfer_encoding = self
|
||||||
|
.find_value("Content-Transfer-Encoding")
|
||||||
|
.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) => Ok(body.get_decoded_as_string()?),
|
||||||
Body::SevenBit(body) | Body::EightBit(body) => Ok(body.get_as_string()?),
|
Body::SevenBit(body) | Body::EightBit(body) => Ok(body.get_as_string()?),
|
||||||
@@ -187,7 +188,9 @@ pub trait Mail<'a, T: Header<'a> + 'a>: HeaderMap<'a, T> {
|
|||||||
|
|
||||||
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.find_value("Content-Transfer-Encoding");
|
let transfer_encoding = self
|
||||||
|
.find_value("Content-Transfer-Encoding")
|
||||||
|
.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()?),
|
||||||
Body::SevenBit(body) | Body::EightBit(body) => Ok(Vec::from(body.get_raw())),
|
Body::SevenBit(body) | Body::EightBit(body) => Ok(Vec::from(body.get_raw())),
|
||||||
@@ -254,7 +257,6 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_content<T: ToString>(&mut self, content: T, ctype: Option<MediaType>) {
|
pub fn set_content<T: ToString>(&mut self, content: T, ctype: Option<MediaType>) {
|
||||||
self.clear_content();
|
|
||||||
let content = content.to_string();
|
let content = content.to_string();
|
||||||
|
|
||||||
let mut ctype = ctype.unwrap_or(MediaType::new(TEXT, PLAIN));
|
let mut ctype = ctype.unwrap_or(MediaType::new(TEXT, PLAIN));
|
||||||
@@ -262,14 +264,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
ctype.set_param(CHARSET, UTF_8);
|
ctype.set_param(CHARSET, UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.headers.add_header("Content-Type", ctype);
|
self.set_content_raw(content, Some(ctype));
|
||||||
|
|
||||||
let (bytes, encoding) = body::encode(content);
|
|
||||||
if encoding != ContentTransferEncoding::SevenBit {
|
|
||||||
self.headers.add_header("Content-Transfer-Encoding", encoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.body_bytes = Cow::Owned(bytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_content_raw<B: Into<BodyContent>>(&mut self, content: B, ctype: Option<MediaType>) {
|
pub fn set_content_raw<B: Into<BodyContent>>(&mut self, content: B, ctype: Option<MediaType>) {
|
||||||
@@ -319,12 +314,13 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
return BodyCheckStatus::NotBody;
|
return BodyCheckStatus::NotBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
let body_types = match bodytype {
|
let subtype = mime.subty();
|
||||||
Some(bodytype) => vec![Name::from(bodytype)],
|
let is_body = match bodytype {
|
||||||
None => vec![PLAIN, HTML],
|
Some(bodytype) => subtype == Name::from(bodytype),
|
||||||
|
None => subtype == PLAIN || subtype == HTML,
|
||||||
};
|
};
|
||||||
|
|
||||||
match body_types.contains(&mime.subty()) {
|
match is_body {
|
||||||
true => BodyCheckStatus::IsBody,
|
true => BodyCheckStatus::IsBody,
|
||||||
false => BodyCheckStatus::NotBody,
|
false => BodyCheckStatus::NotBody,
|
||||||
}
|
}
|
||||||
@@ -403,7 +399,7 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
|
|
||||||
let mut ix = 0;
|
let mut ix = 0;
|
||||||
while ix < self.headers.len() {
|
while ix < self.headers.len() {
|
||||||
if self.headers[ix].get_key().to_ascii_lowercase().starts_with("content-") {
|
if self.headers[ix].key_starts_with("content-") {
|
||||||
subpart.headers.push(self.headers.remove(ix));
|
subpart.headers.push(self.headers.remove(ix));
|
||||||
} else {
|
} else {
|
||||||
ix += 1;
|
ix += 1;
|
||||||
@@ -447,40 +443,29 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
self.make_multipart(RELATED, boundary)
|
self.make_multipart(RELATED, boundary)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_multipart(&mut self, subpart: CowMail<'a, H>, ctype: MediaType) {
|
/// `make_multipart` already returns early when this part is that multipart
|
||||||
match self.is_multipart() {
|
/// subtype, so it covers both wrapping and appending to an existing container.
|
||||||
false => {
|
fn add_multipart(&mut self, subpart: CowMail<'a, H>, subtype: Name) {
|
||||||
self.make_multipart(ctype.subty, None);
|
self.make_multipart(subtype, None);
|
||||||
self.subparts.push(subpart);
|
self.subparts.push(subpart);
|
||||||
}
|
|
||||||
true => {
|
|
||||||
let existing_ctype = self.get_mime_type();
|
|
||||||
if existing_ctype.ty() == MULTIPART && existing_ctype != ctype {
|
|
||||||
self.make_multipart(ctype.subty, None);
|
|
||||||
self.subparts.push(subpart);
|
|
||||||
} else {
|
|
||||||
self.subparts.push(subpart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_related(&mut self, mut subpart: CowMail<'a, H>) {
|
pub fn add_related(&mut self, mut subpart: CowMail<'a, H>) {
|
||||||
if subpart.headers.find_header("Content-Disposition").is_none() {
|
if subpart.headers.find_header("Content-Disposition").is_none() {
|
||||||
subpart.headers.add_header("Content-Disposition", "inline");
|
subpart.headers.add_header("Content-Disposition", "inline");
|
||||||
}
|
}
|
||||||
self.add_multipart(subpart, MediaType::new(MULTIPART, RELATED))
|
self.add_multipart(subpart, RELATED)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_alternative(&mut self, subpart: CowMail<'a, H>) {
|
pub fn add_alternative(&mut self, subpart: CowMail<'a, H>) {
|
||||||
self.add_multipart(subpart, MediaType::new(MULTIPART, ALTERNATIVE))
|
self.add_multipart(subpart, ALTERNATIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_attachment(&mut self, mut subpart: CowMail<'a, H>) {
|
pub fn add_attachment(&mut self, mut subpart: CowMail<'a, H>) {
|
||||||
if subpart.headers.find_header("Content-Disposition").is_none() {
|
if subpart.headers.find_header("Content-Disposition").is_none() {
|
||||||
subpart.headers.add_header("Content-Disposition", "attachment");
|
subpart.headers.add_header("Content-Disposition", "attachment");
|
||||||
}
|
}
|
||||||
self.add_multipart(subpart, MediaType::new(MULTIPART, MIXED))
|
self.add_multipart(subpart, MIXED)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_part_at_path(&self, path: &[usize]) -> &Self {
|
fn get_part_at_path(&self, path: &[usize]) -> &Self {
|
||||||
@@ -612,24 +597,23 @@ impl<'a, H: Header<'a> + 'a> CowMail<'a, H> {
|
|||||||
Cow::Borrowed(body_bytes) => vec![Cow::Borrowed(body_bytes)],
|
Cow::Borrowed(body_bytes) => vec![Cow::Borrowed(body_bytes)],
|
||||||
Cow::Owned(_) => vec![Cow::Borrowed(&self.body_bytes), Cow::Borrowed(self.end_bytes)],
|
Cow::Owned(_) => vec![Cow::Borrowed(&self.body_bytes), Cow::Borrowed(self.end_bytes)],
|
||||||
},
|
},
|
||||||
MessageType::MultiPart(boundary) => std::iter::once(vec![Cow::Borrowed(&*self.body_bytes)])
|
MessageType::MultiPart(boundary) => {
|
||||||
.chain(self.subparts.iter().map(|p| {
|
let delimiter = [b"--", boundary.as_bytes(), b"\r\n"].concat();
|
||||||
let mut res = vec![
|
let closing = [b"--", boundary.as_bytes(), b"--"].concat();
|
||||||
Cow::Borrowed("--".as_bytes()),
|
|
||||||
Cow::Owned(boundary.as_bytes().to_owned()),
|
std::iter::once(vec![Cow::Borrowed(&*self.body_bytes)])
|
||||||
Cow::Borrowed("\r\n".as_bytes()),
|
.chain(self.subparts.iter().map(|p| {
|
||||||
];
|
let mut res = vec![Cow::Owned(delimiter.clone())];
|
||||||
res.append(&mut p.serialize());
|
res.append(&mut p.serialize());
|
||||||
res
|
res
|
||||||
}))
|
}))
|
||||||
.chain(std::iter::once(vec![
|
.chain(std::iter::once(vec![
|
||||||
Cow::Borrowed("--".as_bytes()),
|
Cow::Owned(closing),
|
||||||
Cow::Owned(boundary.as_bytes().to_owned()),
|
Cow::Borrowed(self.end_bytes),
|
||||||
Cow::Borrowed("--".as_bytes()),
|
]))
|
||||||
Cow::Borrowed(self.end_bytes),
|
.flatten()
|
||||||
]))
|
.collect()
|
||||||
.flatten()
|
}
|
||||||
.collect(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,8 +788,7 @@ impl<'a, H: Header<'a> + 'a> Mail<'a, H> for CowMail<'a, H> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear_content(&mut self) {
|
fn clear_content(&mut self) {
|
||||||
self.headers
|
self.headers.retain(|h| !h.key_starts_with("content-"));
|
||||||
.retain(|h| !h.get_key().to_ascii_lowercase().starts_with("content-"));
|
|
||||||
self.body_bytes = Cow::Borrowed(&[]);
|
self.body_bytes = Cow::Borrowed(&[]);
|
||||||
self.subparts.clear();
|
self.subparts.clear();
|
||||||
self.end_bytes = b"\r\n";
|
self.end_bytes = b"\r\n";
|
||||||
|
|||||||
@@ -359,6 +359,25 @@
|
|||||||
assert_eq!(mail.subparts[1].subparts.len(), 2);
|
assert_eq!(mail.subparts[1].subparts.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 2045 section 6.1: Content-Transfer-Encoding values are case insensitive
|
||||||
|
#[test]
|
||||||
|
fn test_uppercase_transfer_encoding_is_decoded() {
|
||||||
|
for encoding in ["base64", "Base64", "BASE64"] {
|
||||||
|
let raw = format!(
|
||||||
|
concat!(
|
||||||
|
"Content-Type: text/plain\n",
|
||||||
|
"Content-Transfer-Encoding: {}\n",
|
||||||
|
"\n",
|
||||||
|
"aGVsbG8gd29ybGQ="
|
||||||
|
),
|
||||||
|
encoding
|
||||||
|
);
|
||||||
|
let mail = CowMail::try_from(raw.as_bytes()).unwrap();
|
||||||
|
assert_eq!(mail.get_content().unwrap(), "hello world", "encoding: {encoding}");
|
||||||
|
assert_eq!(mail.get_content_raw().unwrap(), b"hello world", "encoding: {encoding}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test: related[img, plain] without a `start` param + set_html.
|
// Test: related[img, plain] without a `start` param + set_html.
|
||||||
// The body is not the first subpart, which used to hit unreachable!().
|
// The body is not the first subpart, which used to hit unreachable!().
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user