Replace hyperx ContentType header with our own implementation (#598)

* Replace hyperx ContentType header with our own implementation

* Let's not forget ContentTypeErr

* Adress code review comment
This commit is contained in:
Paolo Barbolini
2021-04-08 10:40:07 +02:00
committed by GitHub
parent acc4ff4898
commit 486e0f9d50
7 changed files with 187 additions and 87 deletions

View File

@@ -29,16 +29,12 @@ fn main() {
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
)
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_HTML)
.body(String::from(html)),
),
)

View File

@@ -38,16 +38,12 @@ fn main() {
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
)
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_HTML)
.body(html.into_string()),
),
)

View File

@@ -0,0 +1,127 @@
use std::{
error::Error as StdError,
fmt::{self, Display, Result as FmtResult},
str::{from_utf8, FromStr},
};
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use mime::Mime;
/// `Content-Type` of the body
///
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
#[derive(Debug, Clone, PartialEq)]
pub struct ContentType(Mime);
impl ContentType {
/// A `ContentType` of type `text/plain; charset=utf-8`
///
/// Indicates that the body is in utf-8 encoded plain text.
pub const TEXT_PLAIN: ContentType = Self::from_mime(mime::TEXT_PLAIN_UTF_8);
/// A `ContentType` of type `text/html; charset=utf-8`
///
/// Indicates that the body is in utf-8 encoded html.
pub const TEXT_HTML: ContentType = Self::from_mime(mime::TEXT_HTML_UTF_8);
/// Parse `s` into `ContentType`
pub fn parse(s: &str) -> Result<ContentType, ContentTypeErr> {
Ok(Self::from_mime(s.parse().map_err(ContentTypeErr)?))
}
pub(crate) const fn from_mime(mime: Mime) -> Self {
Self(mime)
}
pub(crate) fn as_ref(&self) -> &Mime {
&self.0
}
}
impl Header for ContentType {
fn header_name() -> &'static str {
"Content-Type"
}
// FIXME HeaderError->HeaderError, same for result
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one()
.ok_or(HeaderError::Header)
.and_then(|r| from_utf8(r).map_err(|_| HeaderError::Header))
.and_then(|s| s.parse::<Mime>().map(Self).map_err(|_| HeaderError::Header))
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&self.0)
}
}
impl FromStr for ContentType {
type Err = ContentTypeErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
/// An error occurred while trying to [`ContentType::parse`].
#[derive(Debug)]
pub struct ContentTypeErr(mime::FromStrError);
impl StdError for ContentTypeErr {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.0)
}
}
impl Display for ContentTypeErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
#[cfg(test)]
mod test {
use hyperx::header::Headers;
use super::ContentType;
#[test]
fn format_content_type() {
let mut headers = Headers::new();
headers.set(ContentType::TEXT_PLAIN);
assert_eq!(
format!("{}", headers),
"Content-Type: text/plain; charset=utf-8\r\n"
);
headers.set(ContentType::TEXT_HTML);
assert_eq!(
format!("{}", headers),
"Content-Type: text/html; charset=utf-8\r\n"
);
}
#[test]
fn parse_content_type() {
let mut headers = Headers::new();
headers.set_raw("Content-Type", "text/plain; charset=utf-8");
assert_eq!(headers.get::<ContentType>(), Some(&ContentType::TEXT_PLAIN));
headers.set_raw("Content-Type", "text/html; charset=utf-8");
assert_eq!(headers.get::<ContentType>(), Some(&ContentType::TEXT_HTML));
}
}

View File

@@ -1,14 +1,16 @@
//! Headers widely used in email messages
pub use hyperx::header::{
Charset, ContentDisposition, ContentLocation, ContentType, DispositionParam, DispositionType,
Header, Headers,
Charset, ContentDisposition, ContentLocation, DispositionParam, DispositionType, Header,
Headers,
};
pub use self::content_type::{ContentType, ContentTypeErr};
pub use self::date::Date;
pub use self::{content::*, mailbox::*, special::*, textual::*};
mod content;
mod content_type;
mod date;
mod mailbox;
mod special;

View File

@@ -94,7 +94,7 @@ impl Default for SinglePartBuilder {
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let part = SinglePart::builder()
/// .header(header::ContentType("text/plain; charset=utf8".parse()?))
/// .header(header::ContentType::TEXT_PLAIN)
/// .body(String::from("Текст письма в уникоде"));
/// # Ok(())
/// # }
@@ -174,7 +174,7 @@ fn make_boundary() -> String {
}
impl MultiPartKind {
fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
pub(crate) fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
let boundary = boundary.map_or_else(make_boundary, Into::into);
format!(
@@ -217,12 +217,6 @@ impl MultiPartKind {
}
}
impl From<MultiPartKind> for Mime {
fn from(m: MultiPartKind) -> Self {
m.to_mime::<String>(None)
}
}
/// Multipart builder
#[derive(Debug, Clone)]
pub struct MultiPartBuilder {
@@ -245,17 +239,17 @@ impl MultiPartBuilder {
/// Set `Content-Type` header using [`MultiPartKind`]
pub fn kind(self, kind: MultiPartKind) -> Self {
self.header(ContentType(kind.into()))
self.header(ContentType::from_mime(kind.to_mime::<String>(None)))
}
/// Set custom boundary
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
pub fn boundary<S: Into<String>>(self, boundary: S) -> Self {
let kind = {
let mime = &self.headers.get::<ContentType>().unwrap().0;
MultiPartKind::from_mime(mime).unwrap()
let content_type = self.headers.get::<ContentType>().unwrap();
MultiPartKind::from_mime(content_type.as_ref()).unwrap()
};
let mime = kind.to_mime(Some(boundary.as_ref()));
self.header(ContentType(mime))
let mime = kind.to_mime(Some(boundary));
self.header(ContentType::from_mime(mime))
}
/// Creates multipart without parts
@@ -356,8 +350,13 @@ impl MultiPart {
/// Get the boundary of multipart contents
pub fn boundary(&self) -> String {
let content_type = &self.headers.get::<ContentType>().unwrap().0;
content_type.get_param("boundary").unwrap().as_str().into()
let content_type = self.headers.get::<ContentType>().unwrap();
content_type
.as_ref()
.get_param("boundary")
.unwrap()
.as_str()
.into()
}
/// Get the headers from the multipart
@@ -417,16 +416,14 @@ mod test {
#[test]
fn single_part_binary() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n"
@@ -437,16 +434,14 @@ mod test {
#[test]
fn single_part_quoted_printable() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::QuotedPrintable)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
"\r\n",
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
@@ -458,16 +453,14 @@ mod test {
#[test]
fn single_part_base64() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Base64)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
@@ -481,17 +474,13 @@ mod test {
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")),
))
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
@@ -509,12 +498,12 @@ mod test {
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
@@ -527,18 +516,15 @@ mod test {
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"application/pgp-encrypted".parse().unwrap(),
))
.header(header::ContentType::parse("application/pgp-encrypted").unwrap())
.body(String::from("Version: 1")),
))
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/octet-stream; name=\"encrypted.asc\""
.parse()
.header(
ContentType::parse("application/octet-stream; name=\"encrypted.asc\"")
.unwrap(),
))
)
.header(header::ContentDisposition {
disposition: header::DispositionType::Inline,
parameters: vec![header::DispositionParam::Filename(
@@ -586,16 +572,15 @@ mod test {
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType("text/plain".parse().unwrap()))
.header(header::ContentType::TEXT_PLAIN)
.body(String::from("Test email for signature")),
))
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/pgp-signature; name=\"signature.asc\""
.parse()
.header(
ContentType::parse("application/pgp-signature; name=\"signature.asc\"")
.unwrap(),
))
)
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
@@ -622,7 +607,7 @@ mod test {
" micalg=\"pgp-sha256\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Test email for signature\r\n",
@@ -647,11 +632,11 @@ mod test {
let part = MultiPart::alternative()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"))))
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.header(header::ContentType::TEXT_HTML)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
@@ -660,12 +645,12 @@ mod test {
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/html; charset=utf8\r\n",
"Content-Type: text/html; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
@@ -679,16 +664,16 @@ mod test {
.multipart(MultiPart::related()
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.header(header::ContentType::TEXT_HTML)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
.singlepart(SinglePart::builder()
.header(header::ContentType("image/png".parse().unwrap()))
.header(header::ContentType::parse("image/png").unwrap())
.header(header::ContentLocation("/image.png".into()))
.header(header::ContentTransferEncoding::Base64)
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
.singlepart(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".into())]
@@ -705,7 +690,7 @@ mod test {
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
"\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
"Content-Type: text/html; charset=utf8\r\n",
"Content-Type: text/html; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
@@ -719,7 +704,7 @@ mod test {
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",

View File

@@ -75,12 +75,12 @@
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .header(header::ContentType::TEXT_PLAIN)
//! .body(String::from("Hello, world! :)")),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
//! .header(header::ContentType::TEXT_HTML)
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>",
//! )),
@@ -146,23 +146,21 @@
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .header(header::ContentType::TEXT_PLAIN)
//! .body(String::from("Hello, world! :)")),
//! )
//! .multipart(
//! MultiPart::related()
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType(
//! "text/html; charset=utf8".parse()?,
//! ))
//! .header(header::ContentType::TEXT_HTML)
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
//! )),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("image/png".parse()?))
//! .header(header::ContentType::parse("image/png")?)
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Inline,
//! parameters: vec![],
@@ -174,7 +172,7 @@
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .header(header::ContentType::TEXT_PLAIN)
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Attachment,
//! parameters: vec![header::DispositionParam::Filename(
@@ -242,8 +240,6 @@ pub use body::{Body, IntoBody, MaybeString};
pub use mailbox::*;
pub use mimebody::*;
pub use mime;
mod body;
pub mod header;
mod mailbox;
@@ -637,16 +633,14 @@ mod test {
MultiPart::related()
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_HTML)
.body(String::from(
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
)),
)
.singlepart(
SinglePart::builder()
.header(header::ContentType("image/png".parse().unwrap()))
.header(header::ContentType::parse("image/png").unwrap())
.header(header::ContentDisposition {
disposition: header::DispositionType::Inline,
parameters: vec![],

View File

@@ -7,7 +7,7 @@ MIME-Version: 1.0
Content-Type: multipart/related; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
Content-Type: text/html; charset=utf8
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>