diff --git a/Cargo.toml b/Cargo.toml index ef4553a..b8b7f2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ tracing = { version = "0.1.16", default-features = false, features = ["std"], op # builder httpdate = { version = "1", optional = true } -hyperx = { version = "1", optional = true, features = ["headers"] } mime = { version = "0.3.4", optional = true } fastrand = { version = "1.4", optional = true } quoted_printable = { version = "0.4", optional = true } @@ -82,7 +81,7 @@ name = "transport_smtp" [features] default = ["smtp-transport", "native-tls", "hostname", "r2d2", "builder"] -builder = ["httpdate", "mime", "base64", "hyperx", "fastrand", "quoted_printable"] +builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable"] # transports file-transport = ["uuid"] diff --git a/src/address/envelope.rs b/src/address/envelope.rs index 6d5c299..b5f18c1 100644 --- a/src/address/envelope.rs +++ b/src/address/envelope.rs @@ -112,11 +112,11 @@ impl TryFrom<&Headers> for Envelope { fn try_from(headers: &Headers) -> Result { let from = match headers.get::() { // If there is a Sender, use it - Some(sender) => Some(Mailbox::from(sender.clone()).email), + Some(sender) => Some(Mailbox::from(sender).email), // ... else try From None => match headers.get::() { Some(header::From(a)) => { - let from: Vec = a.clone().into(); + let from: Vec = a.into(); if from.len() > 1 { return Err(Error::TooManyFrom); } @@ -128,7 +128,7 @@ impl TryFrom<&Headers> for Envelope { fn add_addresses_from_mailboxes( addresses: &mut Vec
, - mailboxes: Option<&Mailboxes>, + mailboxes: Option, ) { if let Some(mailboxes) = mailboxes { for mailbox in mailboxes.iter() { @@ -137,9 +137,9 @@ impl TryFrom<&Headers> for Envelope { } } let mut to = vec![]; - add_addresses_from_mailboxes(&mut to, headers.get::().map(|h| &h.0)); - add_addresses_from_mailboxes(&mut to, headers.get::().map(|h| &h.0)); - add_addresses_from_mailboxes(&mut to, headers.get::().map(|h| &h.0)); + add_addresses_from_mailboxes(&mut to, headers.get::().map(|h| h.0)); + add_addresses_from_mailboxes(&mut to, headers.get::().map(|h| h.0)); + add_addresses_from_mailboxes(&mut to, headers.get::().map(|h| h.0)); Self::new(from, to) } diff --git a/src/lib.rs b/src/lib.rs index 5704ca3..dd147dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,11 +173,12 @@ pub(crate) type BoxError = Box; #[cfg(test)] #[cfg(feature = "builder")] mod test { - use super::*; - use crate::message::{header, Mailbox, Mailboxes}; - use hyperx::header::Headers; use std::convert::TryFrom; + use super::*; + use crate::message::header::Headers; + use crate::message::{header, Mailbox, Mailboxes}; + #[test] fn envelope_from_headers() { let from = Mailboxes::new().with("kayo@example.com".parse().unwrap()); diff --git a/src/message/header/content.rs b/src/message/header/content.rs index 0f23b02..4e1d3ea 100644 --- a/src/message/header/content.rs +++ b/src/message/header/content.rs @@ -1,12 +1,11 @@ -use hyperx::{ - header::{Formatter as HeaderFormatter, Header, RawLike}, - Error as HeaderError, Result as HyperResult, -}; use std::{ fmt::{Display, Formatter as FmtFormatter, Result as FmtResult}, - str::{from_utf8, FromStr}, + str::FromStr, }; +use super::{Header, HeaderName}; +use crate::BoxError; + /// `Content-Transfer-Encoding` of the body /// /// The `Message` builder takes care of choosing the most @@ -22,9 +21,17 @@ pub enum ContentTransferEncoding { Binary, } -impl Default for ContentTransferEncoding { - fn default() -> Self { - ContentTransferEncoding::Base64 +impl Header for ContentTransferEncoding { + fn name() -> HeaderName { + HeaderName::new_from_ascii_str("Content-Transfer-Encoding") + } + + fn parse(s: &str) -> Result { + Ok(s.parse()?) + } + + fn display(&self) -> String { + self.to_string() } } @@ -54,35 +61,16 @@ impl FromStr for ContentTransferEncoding { } } -impl Header for ContentTransferEncoding { - fn header_name() -> &'static str { - "Content-Transfer-Encoding" - } - - // FIXME HeaderError->HeaderError, same for result - fn parse_header<'a, T>(raw: &'a T) -> HyperResult - 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::() - .map_err(|_| HeaderError::Header) - }) - } - - fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { - f.fmt_line(&format!("{}", self)) +impl Default for ContentTransferEncoding { + fn default() -> Self { + ContentTransferEncoding::Base64 } } #[cfg(test)] mod test { use super::ContentTransferEncoding; - use hyperx::header::Headers; + use crate::message::header::{HeaderName, Headers}; #[test] fn format_content_transfer_encoding() { @@ -90,35 +78,35 @@ mod test { headers.set(ContentTransferEncoding::SevenBit); - assert_eq!( - format!("{}", headers), - "Content-Transfer-Encoding: 7bit\r\n" - ); + assert_eq!(headers.to_string(), "Content-Transfer-Encoding: 7bit\r\n"); headers.set(ContentTransferEncoding::Base64); - assert_eq!( - format!("{}", headers), - "Content-Transfer-Encoding: base64\r\n" - ); + assert_eq!(headers.to_string(), "Content-Transfer-Encoding: base64\r\n"); } #[test] fn parse_content_transfer_encoding() { let mut headers = Headers::new(); - headers.set_raw("Content-Transfer-Encoding", "7bit"); - - assert_eq!( - headers.get::(), - Some(&ContentTransferEncoding::SevenBit) + headers.set_raw( + HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), + "7bit".to_string(), ); - headers.set_raw("Content-Transfer-Encoding", "base64"); + assert_eq!( + headers.get::(), + Some(ContentTransferEncoding::SevenBit) + ); + + headers.set_raw( + HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), + "base64".to_string(), + ); assert_eq!( headers.get::(), - Some(&ContentTransferEncoding::Base64) + Some(ContentTransferEncoding::Base64) ); } } diff --git a/src/message/header/content_disposition.rs b/src/message/header/content_disposition.rs index 4a8fb7b..d1dd77b 100644 --- a/src/message/header/content_disposition.rs +++ b/src/message/header/content_disposition.rs @@ -1,9 +1,5 @@ -use std::{fmt::Result as FmtResult, str::from_utf8}; - -use hyperx::{ - header::{Formatter as HeaderFormatter, Header, RawLike}, - Error as HeaderError, Result as HyperResult, -}; +use super::{Header, HeaderName}; +use crate::BoxError; /// `Content-Disposition` of an attachment /// @@ -32,31 +28,23 @@ impl ContentDisposition { } impl Header for ContentDisposition { - fn header_name() -> &'static str { - "Content-Disposition" + fn name() -> HeaderName { + HeaderName::new_from_ascii_str("Content-Disposition") } - // FIXME HeaderError->HeaderError, same for result - fn parse_header<'a, T>(raw: &'a T) -> HyperResult - where - T: RawLike<'a>, - Self: Sized, - { - raw.one() - .ok_or(HeaderError::Header) - .and_then(|r| from_utf8(r).map_err(|_| HeaderError::Header)) - .map(|s| Self(s.into())) + fn parse(s: &str) -> Result { + Ok(Self(s.into())) } - fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { - f.fmt_line(&self.0) + fn display(&self) -> String { + self.0.clone() } } #[cfg(test)] mod test { use super::ContentDisposition; - use hyperx::header::Headers; + use crate::message::header::{HeaderName, Headers}; #[test] fn format_content_disposition() { @@ -78,21 +66,24 @@ mod test { fn parse_content_disposition() { let mut headers = Headers::new(); - headers.set_raw("Content-Disposition", "inline"); + headers.set_raw( + HeaderName::new_from_ascii_str("Content-Disposition"), + "inline".to_string(), + ); assert_eq!( headers.get::(), - Some(&ContentDisposition::inline()) + Some(ContentDisposition::inline()) ); headers.set_raw( - "Content-Disposition", - "attachment; filename=\"something.txt\"", + HeaderName::new_from_ascii_str("Content-Disposition"), + "attachment; filename=\"something.txt\"".to_string(), ); assert_eq!( headers.get::(), - Some(&ContentDisposition::attachment("something.txt")) + Some(ContentDisposition::attachment("something.txt")) ); } } diff --git a/src/message/header/content_type.rs b/src/message/header/content_type.rs index 0d17673..f563bce 100644 --- a/src/message/header/content_type.rs +++ b/src/message/header/content_type.rs @@ -1,15 +1,14 @@ use std::{ error::Error as StdError, - fmt::{self, Display, Result as FmtResult}, - str::{from_utf8, FromStr}, + fmt::{self, Display}, + str::FromStr, }; -use hyperx::{ - header::{Formatter as HeaderFormatter, Header, RawLike}, - Error as HeaderError, Result as HyperResult, -}; use mime::Mime; +use super::{Header, HeaderName}; +use crate::BoxError; + /// `Content-Type` of the body /// /// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5) @@ -42,24 +41,16 @@ impl ContentType { } impl Header for ContentType { - fn header_name() -> &'static str { - "Content-Type" + fn name() -> HeaderName { + HeaderName::new_from_ascii_str("Content-Type") } - // FIXME HeaderError->HeaderError, same for result - fn parse_header<'a, T>(raw: &'a T) -> HyperResult - 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::().map(Self).map_err(|_| HeaderError::Header)) + fn parse(s: &str) -> Result { + Ok(Self(s.parse()?)) } - fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { - f.fmt_line(&self.0) + fn display(&self) -> String { + self.0.to_string() } } @@ -89,9 +80,8 @@ impl Display for ContentTypeErr { #[cfg(test)] mod test { - use hyperx::header::Headers; - use super::ContentType; + use crate::message::header::{HeaderName, Headers}; #[test] fn format_content_type() { @@ -100,14 +90,14 @@ mod test { headers.set(ContentType::TEXT_PLAIN); assert_eq!( - format!("{}", headers), + headers.to_string(), "Content-Type: text/plain; charset=utf-8\r\n" ); headers.set(ContentType::TEXT_HTML); assert_eq!( - format!("{}", headers), + headers.to_string(), "Content-Type: text/html; charset=utf-8\r\n" ); } @@ -116,12 +106,18 @@ mod test { fn parse_content_type() { let mut headers = Headers::new(); - headers.set_raw("Content-Type", "text/plain; charset=utf-8"); + headers.set_raw( + HeaderName::new_from_ascii_str("Content-Type"), + "text/plain; charset=utf-8".to_string(), + ); - assert_eq!(headers.get::(), Some(&ContentType::TEXT_PLAIN)); + assert_eq!(headers.get::(), Some(ContentType::TEXT_PLAIN)); - headers.set_raw("Content-Type", "text/html; charset=utf-8"); + headers.set_raw( + HeaderName::new_from_ascii_str("Content-Type"), + "text/html; charset=utf-8".to_string(), + ); - assert_eq!(headers.get::(), Some(&ContentType::TEXT_HTML)); + assert_eq!(headers.get::(), Some(ContentType::TEXT_HTML)); } } diff --git a/src/message/header/date.rs b/src/message/header/date.rs index 23480d2..fc5f8e6 100644 --- a/src/message/header/date.rs +++ b/src/message/header/date.rs @@ -1,10 +1,9 @@ -use std::{fmt::Result as FmtResult, str::from_utf8, time::SystemTime}; +use std::time::SystemTime; use httpdate::HttpDate; -use hyperx::{ - header::{Formatter as HeaderFormatter, Header, RawLike}, - Error as HeaderError, Result as HyperResult, -}; + +use super::{Header, HeaderName}; +use crate::BoxError; /// Message `Date` header /// @@ -27,36 +26,24 @@ impl Date { } impl Header for Date { - fn header_name() -> &'static str { - "Date" + fn name() -> HeaderName { + HeaderName::new_from_ascii_str("Date") } - // FIXME HeaderError->HeaderError, same for result - fn parse_header<'a, T>(raw: &'a T) -> HyperResult - 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| { - let mut s = String::from(s); - if s.ends_with(" -0000") { - // The httpdate crate expects the `Date` to end in ` GMT`, but email - // uses `-0000`, so we crudely fix this issue here. + fn parse(s: &str) -> Result { + let mut s = String::from(s); + if s.ends_with(" -0000") { + // The httpdate crate expects the `Date` to end in ` GMT`, but email + // uses `-0000`, so we crudely fix this issue here. - s.truncate(s.len() - "-0000".len()); - s.push_str("GMT"); - } + s.truncate(s.len() - "-0000".len()); + s.push_str("GMT"); + } - s.parse::() - .map(Self) - .map_err(|_| HeaderError::Header) - }) + Ok(Self(s.parse::()?)) } - fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { + fn display(&self) -> String { let mut s = self.0.to_string(); if s.ends_with(" GMT") { // The httpdate crate always appends ` GMT` to the end of the string, @@ -67,7 +54,7 @@ impl Header for Date { s.push_str("-0000"); } - f.fmt_line(&s) + s } } @@ -85,10 +72,10 @@ impl From for SystemTime { #[cfg(test)] mod test { - use hyperx::header::Headers; use std::time::{Duration, SystemTime}; use super::Date; + use crate::message::header::{HeaderName, Headers}; #[test] fn format_date() { @@ -100,8 +87,8 @@ mod test { )); assert_eq!( - format!("{}", headers), - "Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n" + headers.to_string(), + "Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n".to_string() ); // Tue, 15 Nov 1994 08:12:32 GMT @@ -110,7 +97,7 @@ mod test { )); assert_eq!( - format!("{}", headers), + headers.to_string(), "Date: Tue, 15 Nov 1994 08:12:32 -0000\r\n" ); } @@ -119,20 +106,26 @@ mod test { fn parse_date() { let mut headers = Headers::new(); - headers.set_raw("Date", "Tue, 15 Nov 1994 08:12:31 -0000"); + headers.set_raw( + HeaderName::new_from_ascii_str("Date"), + "Tue, 15 Nov 1994 08:12:31 -0000".to_string(), + ); assert_eq!( headers.get::(), - Some(&Date::from( + Some(Date::from( SystemTime::UNIX_EPOCH + Duration::from_secs(784887151), )) ); - headers.set_raw("Date", "Tue, 15 Nov 1994 08:12:32 -0000"); + headers.set_raw( + HeaderName::new_from_ascii_str("Date"), + "Tue, 15 Nov 1994 08:12:32 -0000".to_string(), + ); assert_eq!( headers.get::(), - Some(&Date::from( + Some(Date::from( SystemTime::UNIX_EPOCH + Duration::from_secs(784887152), )) ); diff --git a/src/message/header/mailbox.rs b/src/message/header/mailbox.rs index 19a336f..bed4170 100644 --- a/src/message/header/mailbox.rs +++ b/src/message/header/mailbox.rs @@ -1,12 +1,6 @@ -use crate::message::{ - mailbox::{Mailbox, Mailboxes}, - utf8_b, -}; -use hyperx::{ - header::{Formatter as HeaderFormatter, Header, RawLike}, - Error as HeaderError, Result as HyperResult, -}; -use std::{fmt::Result as FmtResult, slice::Iter, str::from_utf8}; +use super::{Header, HeaderName}; +use crate::message::mailbox::{Mailbox, Mailboxes}; +use crate::BoxError; /// Header which can contains multiple mailboxes pub trait MailboxesHeader { @@ -20,23 +14,17 @@ macro_rules! mailbox_header { pub struct $type_name(Mailbox); impl Header for $type_name { - fn header_name() -> &'static str { - $header_name + fn name() -> HeaderName { + HeaderName::new_from_ascii_str($header_name) } - fn parse_header<'a, T>(raw: &'a T) -> HyperResult where - T: RawLike<'a>, - Self: Sized { - raw.one() - .ok_or(HeaderError::Header) - .and_then(parse_mailboxes) - .and_then(|mbs| { - mbs.into_single().ok_or(HeaderError::Header) - }).map($type_name) + fn parse(s: &str) -> Result { + let mailbox: Mailbox = s.parse()?; + Ok(Self(mailbox)) } - fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { - f.fmt_line(&self.0.recode_name(utf8_b::encode)) + fn display(&self) -> String { + self.0.to_string() } } @@ -69,23 +57,17 @@ macro_rules! mailboxes_header { } impl Header for $type_name { - fn header_name() -> &'static str { - $header_name + fn name() -> HeaderName { + HeaderName::new_from_ascii_str($header_name) } - fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name> - where - T: RawLike<'a>, - Self: Sized, - { - raw.one() - .ok_or(HeaderError::Header) - .and_then(parse_mailboxes) - .map($type_name) + fn parse(s: &str) -> Result { + let mailbox: Mailboxes = s.parse()?; + Ok(Self(mailbox)) } - fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { - format_mailboxes(self.0.iter(), f) + fn display(&self) -> String { + self.0.to_string() } } @@ -174,26 +156,10 @@ mailboxes_header! { (Bcc, "Bcc") } -fn parse_mailboxes(raw: &[u8]) -> HyperResult { - if let Ok(src) = from_utf8(raw) { - if let Ok(mbs) = src.parse() { - return Ok(mbs); - } - } - Err(HeaderError::Header) -} - -fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { - f.fmt_line(&Mailboxes::from( - mbs.map(|mb| mb.recode_name(utf8_b::encode)) - .collect::>(), - )) -} - #[cfg(test)] mod test { use super::{From, Mailbox, Mailboxes}; - use hyperx::header::Headers; + use crate::message::header::{HeaderName, Headers}; #[test] fn format_single_without_name() { @@ -202,7 +168,7 @@ mod test { let mut headers = Headers::new(); headers.set(From(from)); - assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n"); + assert_eq!(headers.to_string(), "From: kayo@example.com\r\n"); } #[test] @@ -212,7 +178,7 @@ mod test { let mut headers = Headers::new(); headers.set(From(from)); - assert_eq!(format!("{}", headers), "From: K. \r\n"); + assert_eq!(headers.to_string(), "From: K. \r\n"); } #[test] @@ -225,7 +191,7 @@ mod test { headers.set(From(from)); assert_eq!( - format!("{}", headers), + headers.to_string(), "From: kayo@example.com, pony@domain.tld\r\n" ); } @@ -241,7 +207,7 @@ mod test { headers.set(From(from.into())); assert_eq!( - format!("{}", headers), + headers.to_string(), "From: K. , Pony P. \r\n" ); } @@ -254,7 +220,7 @@ mod test { headers.set(From(from.into())); assert_eq!( - format!("{}", headers), + headers.to_string(), "From: =?utf-8?b?0JrQsNC50L4=?= \r\n" ); } @@ -264,9 +230,12 @@ mod test { let from = vec!["kayo@example.com".parse().unwrap()].into(); let mut headers = Headers::new(); - headers.set_raw("From", "kayo@example.com"); + headers.set_raw( + HeaderName::new_from_ascii_str("From"), + "kayo@example.com".to_string(), + ); - assert_eq!(headers.get::(), Some(&From(from))); + assert_eq!(headers.get::(), Some(From(from))); } #[test] @@ -274,9 +243,12 @@ mod test { let from = vec!["K. ".parse().unwrap()].into(); let mut headers = Headers::new(); - headers.set_raw("From", "K. "); + headers.set_raw( + HeaderName::new_from_ascii_str("From"), + "K. ".to_string(), + ); - assert_eq!(headers.get::(), Some(&From(from))); + assert_eq!(headers.get::(), Some(From(from))); } #[test] @@ -287,9 +259,12 @@ mod test { ]; let mut headers = Headers::new(); - headers.set_raw("From", "kayo@example.com, pony@domain.tld"); + headers.set_raw( + HeaderName::new_from_ascii_str("From"), + "kayo@example.com, pony@domain.tld".to_string(), + ); - assert_eq!(headers.get::(), Some(&From(from.into()))); + assert_eq!(headers.get::(), Some(From(from.into()))); } #[test] @@ -300,18 +275,11 @@ mod test { ]; let mut headers = Headers::new(); - headers.set_raw("From", "K. , Pony P. "); + headers.set_raw( + HeaderName::new_from_ascii_str("From"), + "K. , Pony P. ".to_string(), + ); - assert_eq!(headers.get::(), Some(&From(from.into()))); - } - - #[test] - fn parse_single_with_utf8_name() { - let from: Vec = vec!["Кайо ".parse().unwrap()]; - - let mut headers = Headers::new(); - headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= "); - - assert_eq!(headers.get::(), Some(&From(from.into()))); + assert_eq!(headers.get::(), Some(From(from.into()))); } } diff --git a/src/message/header/mod.rs b/src/message/header/mod.rs index e0fe907..79fa584 100644 --- a/src/message/header/mod.rs +++ b/src/message/header/mod.rs @@ -1,11 +1,16 @@ //! Headers widely used in email messages -pub use hyperx::header::{Charset, Header, Headers}; +use std::{ + borrow::Cow, + fmt::{self, Display, Formatter}, + ops::Deref, +}; pub use self::content_disposition::ContentDisposition; pub use self::content_type::{ContentType, ContentTypeErr}; pub use self::date::Date; pub use self::{content::*, mailbox::*, special::*, textual::*}; +use crate::BoxError; mod content; mod content_disposition; @@ -14,3 +19,718 @@ mod date; mod mailbox; mod special; mod textual; + +pub trait Header: Clone { + fn name() -> HeaderName; + + fn parse(s: &str) -> Result; + + fn display(&self) -> String; +} + +#[derive(Debug, Clone, Default)] +pub struct Headers { + headers: Vec<(HeaderName, String)>, +} + +impl Headers { + #[inline] + pub const fn new() -> Self { + Self { + headers: Vec::new(), + } + } + + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + Self { + headers: Vec::with_capacity(capacity), + } + } + + pub fn get(&self) -> Option { + self.get_raw(&H::name()).and_then(|raw| H::parse(raw).ok()) + } + + pub fn set(&mut self, header: H) { + self.set_raw(H::name(), header.display()); + } + + pub fn remove(&mut self) -> Option { + self.remove_raw(&H::name()) + .and_then(|(_name, raw)| H::parse(&raw).ok()) + } + + #[inline] + pub fn clear(&mut self) { + self.headers.clear(); + } + + pub fn get_raw(&self, name: &str) -> Option<&str> { + self.find_header(name).map(|(_name, value)| value) + } + + pub fn insert_raw(&mut self, name: HeaderName, value: String) { + match self.find_header_mut(&name) { + Some((_name, prev_value)) => { + prev_value.push_str(", "); + prev_value.push_str(&value); + } + None => self.headers.push((name, value)), + } + } + + pub fn set_raw(&mut self, name: HeaderName, value: String) { + match self.find_header_mut(&name) { + Some((_, current_value)) => { + *current_value = value; + } + None => { + self.headers.push((name, value)); + } + } + } + + pub fn remove_raw(&mut self, name: &str) -> Option<(HeaderName, String)> { + self.find_header_index(name).map(|i| self.headers.remove(i)) + } + + fn find_header(&self, name: &str) -> Option<(&HeaderName, &str)> { + self.headers + .iter() + .find(|&(name_, _value)| name.eq_ignore_ascii_case(name_)) + .map(|t| (&t.0, t.1.as_str())) + } + + fn find_header_mut(&mut self, name: &str) -> Option<(&HeaderName, &mut String)> { + self.headers + .iter_mut() + .find(|(name_, _value)| name.eq_ignore_ascii_case(name_)) + .map(|t| (&t.0, &mut t.1)) + } + + fn find_header_index(&self, name: &str) -> Option { + self.headers + .iter() + .enumerate() + .find(|&(_i, (name_, _value))| name.eq_ignore_ascii_case(name_)) + .map(|(i, _)| i) + } +} + +impl Display for Headers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (name, value) in &self.headers { + Display::fmt(name, f)?; + f.write_str(": ")?; + HeaderValueEncoder::encode(&name, &value, f)?; + f.write_str("\r\n")?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct HeaderName(Cow<'static, str>); + +impl HeaderName { + pub fn new_from_ascii(ascii: String) -> Option { + if !ascii.is_empty() + && ascii.len() <= 76 + && ascii.is_ascii() + && !ascii.contains(|c| c == ':' || c == ' ') + { + Some(Self(Cow::Owned(ascii))) + } else { + None + } + } + + pub const fn new_from_ascii_str(ascii: &'static str) -> Self { + macro_rules! static_assert { + ($condition:expr) => { + let _ = [()][(!($condition)) as usize]; + }; + } + + static_assert!(!ascii.is_empty()); + static_assert!(ascii.len() <= 76); + + let bytes = ascii.as_bytes(); + let mut i = 0; + while i < bytes.len() { + static_assert!(bytes[i].is_ascii()); + static_assert!(bytes[i] != b' '); + static_assert!(bytes[i] != b':'); + + i += 1; + } + + Self(Cow::Borrowed(ascii)) + } +} + +impl Display for HeaderName { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(&self) + } +} + +impl Deref for HeaderName { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for HeaderName { + #[inline] + fn as_ref(&self) -> &[u8] { + let s: &str = self.as_ref(); + s.as_bytes() + } +} + +impl AsRef for HeaderName { + #[inline] + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl PartialEq for HeaderName { + fn eq(&self, other: &HeaderName) -> bool { + let s1: &str = self.as_ref(); + let s2: &str = other.as_ref(); + s1 == s2 + } +} + +impl PartialEq<&str> for HeaderName { + fn eq(&self, other: &&str) -> bool { + let s: &str = self.as_ref(); + s == *other + } +} + +impl PartialEq for &str { + fn eq(&self, other: &HeaderName) -> bool { + let s: &str = other.as_ref(); + *self == s + } +} + +const ENCODING_START_PREFIX: &str = "=?utf-8?b?"; +const ENCODING_END_SUFFIX: &str = "?="; +const MAX_LINE_LEN: usize = 76; + +/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder +struct HeaderValueEncoder { + line_len: usize, + encode_buf: String, +} + +impl HeaderValueEncoder { + fn encode(name: &str, value: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (words_iter, encoder) = Self::new(name, value); + encoder.format(words_iter, f) + } + + fn new<'a>(name: &str, value: &'a str) -> (WordsPlusFillIterator<'a>, Self) { + ( + WordsPlusFillIterator { s: value }, + Self { + line_len: name.len() + ": ".len(), + encode_buf: String::new(), + }, + ) + } + + fn format( + mut self, + words_iter: WordsPlusFillIterator<'_>, + f: &mut fmt::Formatter<'_>, + ) -> fmt::Result { + /// Estimate if an encoded string of `len` would fix in an empty line + fn would_fit_new_line(len: usize) -> bool { + len < (MAX_LINE_LEN - " ".len()) + } + + /// Estimate how long a string of `len` would be after base64 encoding plus + /// adding the encoding prefix and suffix to it + fn base64_len(len: usize) -> usize { + ENCODING_START_PREFIX.len() + (len * 4 / 3 + 4) + ENCODING_END_SUFFIX.len() + } + + /// Estimate how many more bytes we can fit in the current line + fn available_len_to_max_encode_len(len: usize) -> usize { + len.saturating_sub( + ENCODING_START_PREFIX.len() + (len * 3 / 4 + 4) + ENCODING_END_SUFFIX.len(), + ) + } + + for next_word in words_iter { + let allowed = allowed_str(next_word); + + if allowed { + // This word only contains allowed characters + + // the next word is allowed, but we may have accumulated some words to encode + self.flush_encode_buf(f, true)?; + + if next_word.len() > self.remaining_line_len() { + // not enough space left on this line to encode word + + if self.something_written_to_this_line() && would_fit_new_line(next_word.len()) + { + // word doesn't fit this line, but something had already been written to it, + // and word would fit the next line, so go to a new line + // so go to new line + self.new_line(f)?; + } else { + // word neither fits this line and the next one, cut it + // in the middle and make it fit + + let mut next_word = next_word; + + while !next_word.is_empty() { + if self.remaining_line_len() == 0 { + self.new_line(f)?; + } + + let len = self.remaining_line_len().min(next_word.len()); + let first_part = &next_word[..len]; + next_word = &next_word[len..]; + + f.write_str(first_part)?; + self.line_len += first_part.len(); + } + + continue; + } + } + + // word fits, write it! + f.write_str(next_word)?; + self.line_len += next_word.len(); + } else { + // This word contains unallowed characters + + if self.remaining_line_len() >= base64_len(self.encode_buf.len() + next_word.len()) + { + // next_word fits + self.encode_buf.push_str(next_word); + continue; + } + + // next_word doesn't fit this line + + if would_fit_new_line(base64_len(next_word.len())) { + // ...but it would fit the next one + + self.flush_encode_buf(f, false)?; + self.new_line(f)?; + + self.encode_buf.push_str(next_word); + continue; + } + + // ...and also wouldn't fit the next one. + // chop it up into pieces + + let mut next_word = next_word; + + while !next_word.is_empty() { + if self.remaining_line_len() <= base64_len(1) { + self.flush_encode_buf(f, false)?; + self.new_line(f)?; + } + + let mut len = available_len_to_max_encode_len(self.remaining_line_len()) + .min(next_word.len()); + // avoid slicing on a char boundary + while !next_word.is_char_boundary(len) { + len += 1; + } + let first_part = &next_word[..len]; + next_word = &next_word[len..]; + + self.encode_buf.push_str(first_part); + } + } + } + + self.flush_encode_buf(f, false)?; + + Ok(()) + } + + /// Returns the number of bytes left for the current line + fn remaining_line_len(&self) -> usize { + MAX_LINE_LEN - self.line_len + } + + /// Returns true if something has been written to the current line + fn something_written_to_this_line(&self) -> bool { + self.line_len > 1 + } + + fn flush_encode_buf( + &mut self, + f: &mut fmt::Formatter<'_>, + switching_to_allowed: bool, + ) -> fmt::Result { + use std::fmt::Write; + + if self.encode_buf.is_empty() { + // nothing to encode + return Ok(()); + } + + let mut write_after = None; + + if switching_to_allowed { + // If the next word only contains allowed characters, and the string to encode + // ends with a space, take the space out of the part to encode + + let last_char = self.encode_buf.pop().expect("self.encode_buf isn't empty"); + if is_space_like(last_char) { + write_after = Some(last_char); + } else { + self.encode_buf.push(last_char); + } + } + + f.write_str(ENCODING_START_PREFIX)?; + let encoded = base64::display::Base64Display::with_config( + self.encode_buf.as_bytes(), + base64::STANDARD, + ); + Display::fmt(&encoded, f)?; + f.write_str(ENCODING_END_SUFFIX)?; + + self.line_len += ENCODING_START_PREFIX.len(); + self.line_len += self.encode_buf.len() * 4 / 3 + 4; + self.line_len += ENCODING_END_SUFFIX.len(); + + if let Some(write_after) = write_after { + f.write_char(write_after)?; + self.line_len += 1; + } + + self.encode_buf.clear(); + Ok(()) + } + + fn new_line(&mut self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("\r\n ")?; + self.line_len = 1; + + Ok(()) + } +} + +/// Iterator yielding a string split space by space, but including all space +/// characters between it and the next word +struct WordsPlusFillIterator<'a> { + s: &'a str, +} + +impl<'a> Iterator for WordsPlusFillIterator<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + if self.s.is_empty() { + return None; + } + + let next_word = self + .s + .char_indices() + .skip(1) + .skip_while(|&(_i, c)| !is_space_like(c)) + .find(|&(_i, c)| !is_space_like(c)) + .map(|(i, _)| i); + + let word = &self.s[..next_word.unwrap_or_else(|| self.s.len())]; + self.s = &self.s[word.len()..]; + Some(word) + } +} + +const fn is_space_like(c: char) -> bool { + c == ',' || c == ' ' +} + +fn allowed_str(s: &str) -> bool { + s.chars().all(allowed_char) +} + +const fn allowed_char(c: char) -> bool { + c >= 1 as char && c <= 9 as char + || c == 11 as char + || c == 12 as char + || c >= 14 as char && c <= 127 as char +} + +#[cfg(test)] +mod tests { + use super::{HeaderName, Headers}; + + #[test] + fn valid_headername() { + assert!(HeaderName::new_from_ascii(String::from("From")).is_some()); + } + + #[test] + fn non_ascii_headername() { + assert!(HeaderName::new_from_ascii(String::from("🌎")).is_none()); + } + + #[test] + fn spaces_in_headername() { + assert!(HeaderName::new_from_ascii(String::from("From ")).is_none()); + } + + #[test] + fn colons_in_headername() { + assert!(HeaderName::new_from_ascii(String::from("From:")).is_none()); + } + + #[test] + fn empty_headername() { + assert!(HeaderName::new_from_ascii(String::from("")).is_none()); + } + + #[test] + fn const_valid_headername() { + let _ = HeaderName::new_from_ascii_str("From"); + } + + #[test] + #[should_panic] + fn const_non_ascii_headername() { + let _ = HeaderName::new_from_ascii_str("🌎"); + } + + #[test] + #[should_panic] + fn const_spaces_in_headername() { + let _ = HeaderName::new_from_ascii_str("From "); + } + + #[test] + #[should_panic] + fn const_colons_in_headername() { + let _ = HeaderName::new_from_ascii_str("From:"); + } + + #[test] + #[should_panic] + fn const_empty_headername() { + let _ = HeaderName::new_from_ascii_str(""); + } + + // names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino + + #[test] + fn format_ascii() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("To"), + "John Doe , Jean Dupont ".to_string(), + ); + + assert_eq!( + headers.to_string(), + "To: John Doe , Jean Dupont \r\n" + ); + } + + #[test] + fn format_ascii_with_folding() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("To"), + "Ascii , John Doe , Pinco Pallino , Jemand , Jean Dupont ".to_string(), + ); + + assert_eq!( + headers.to_string(), + concat!( + "To: Ascii , John Doe , Pinco Pallino , Jemand \r\n", + " , Jean Dupont \r\n" + ) + ); + } + + #[test] + fn format_ascii_with_folding_long_line() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("Subject"), + "Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string() + ); + + assert_eq!( + headers.to_string(), + concat!( + "Subject: Hello! This is lettre, and this \r\n ", + "IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n", + " guess that's it!\r\n" + ) + ); + } + + #[test] + fn format_ascii_with_folding_very_long_line() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("Subject"), + "Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string() + ); + + assert_eq!( + headers.to_string(), + concat!( + "Subject: Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoY\r\n", + " ouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know\r\n", + ) + ); + } + + #[test] + fn format_ascii_with_folding_giant_word() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("Subject"), + "1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string() + ); + + assert_eq!( + headers.to_string(), + concat!( + "Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijkl\r\n", + " mnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdef\r\n", + " ghijklmnopqrstuvwxyz\r\n", + ) + ); + } + + #[test] + fn format_special() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("To"), + "Seán ".to_string(), + ); + + assert_eq!( + headers.to_string(), + "To: =?utf-8?b?U2XDoW4=?= \r\n" + ); + } + + #[test] + fn format_special_emoji() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("To"), + "🌎 ".to_string(), + ); + + assert_eq!( + headers.to_string(), + "To: =?utf-8?b?8J+Mjg==?= \r\n" + ); + } + + #[test] + fn format_special_with_folding() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("To"), + "🌍 , 🦆 Everywhere , Иванов Иван Иванович , Jānis Bērziņš , Seán Ó Rudaí ".to_string(), + ); + + assert_eq!( + headers.to_string(), + concat!( + "To: =?utf-8?b?8J+MjQ==?= , =?utf-8?b?8J+mhg==?= \r\n", + " Everywhere , =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n", + " =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= , \r\n", + " =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= , \r\n", + " =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= \r\n" + ) + ); + } + + #[test] + fn format_slice_on_char_boundary_bug() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("Subject"), + "🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(), + ); + + assert_eq!( + headers.to_string(), + "Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n" + ); + } + + #[test] + fn format_bad_stuff() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("Subject"), + "Hello! \r\n This is \" bad \0. 👋".to_string(), + ); + + assert_eq!( + headers.to_string(), + "Subject: Hello! =?utf-8?b?DQo=?= This is \" bad =?utf-8?b?AC4g8J+Riw==?=\r\n" + ); + } + + #[test] + fn format_everything() { + let mut headers = Headers::new(); + headers.insert_raw( + HeaderName::new_from_ascii_str("Subject"), + "Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string() + ); + headers.insert_raw( + HeaderName::new_from_ascii_str("To"), + "🌍 , 🦆 Everywhere , Иванов Иван Иванович , Jānis Bērziņš , Seán Ó Rudaí ".to_string(), + ); + headers.insert_raw( + HeaderName::new_from_ascii_str("From"), + "Someone ".to_string(), + ); + headers.insert_raw( + HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), + "quoted-printable".to_string(), + ); + + assert_eq!( + headers.to_string(), + concat!( + "Subject: Hello! This is lettre, and this \r\n", + " IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n", + " guess that's it!\r\n", + "To: =?utf-8?b?8J+MjQ==?= , =?utf-8?b?8J+mhg==?= \r\n", + " Everywhere , =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n", + " =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= , \r\n", + " =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= , \r\n", + " =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= \r\n", + "From: Someone \r\n", + "Content-Transfer-Encoding: quoted-printable\r\n", + ) + ); + } +} diff --git a/src/message/header/special.rs b/src/message/header/special.rs index 4cbba74..c5c879d 100644 --- a/src/message/header/special.rs +++ b/src/message/header/special.rs @@ -1,11 +1,8 @@ -use hyperx::{ - header::{Formatter as HeaderFormatter, Header, RawLike}, - Error as HeaderError, Result as HyperResult, -}; -use std::{fmt::Result as FmtResult, str::from_utf8}; +use crate::message::header::{Header, HeaderName}; +use crate::BoxError; -#[derive(Debug, Copy, Clone, PartialEq)] /// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4) +#[derive(Debug, Copy, Clone, PartialEq)] pub struct MimeVersion { major: u8, minor: u8, @@ -29,42 +26,40 @@ impl MimeVersion { } } +impl Header for MimeVersion { + fn name() -> HeaderName { + HeaderName::new_from_ascii_str("MIME-Version") + } + + fn parse(s: &str) -> Result { + let mut s = s.split('.'); + + let major = s + .next() + .expect("The first call to next for a Split always succeeds"); + let minor = s + .next() + .ok_or_else(|| String::from("MIME-Version header doesn't contain '.'"))?; + let major = major.parse()?; + let minor = minor.parse()?; + Ok(MimeVersion::new(major, minor)) + } + + fn display(&self) -> String { + format!("{}.{}", self.major, self.minor) + } +} + impl Default for MimeVersion { fn default() -> Self { MIME_VERSION_1_0 } } -impl Header for MimeVersion { - fn header_name() -> &'static str { - "MIME-Version" - } - - fn parse_header<'a, T>(raw: &'a T) -> HyperResult - where - T: RawLike<'a>, - Self: Sized, - { - raw.one().ok_or(HeaderError::Header).and_then(|r| { - let mut s = from_utf8(r).map_err(|_| HeaderError::Header)?.split('.'); - - let major = s.next().ok_or(HeaderError::Header)?; - let minor = s.next().ok_or(HeaderError::Header)?; - let major = major.parse().map_err(|_| HeaderError::Header)?; - let minor = minor.parse().map_err(|_| HeaderError::Header)?; - Ok(MimeVersion::new(major, minor)) - }) - } - - fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { - f.fmt_line(&format!("{}.{}", self.major, self.minor)) - } -} - #[cfg(test)] mod test { use super::{MimeVersion, MIME_VERSION_1_0}; - use hyperx::header::Headers; + use crate::message::header::{HeaderName, Headers}; #[test] fn format_mime_version() { @@ -72,23 +67,29 @@ mod test { headers.set(MIME_VERSION_1_0); - assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n"); + assert_eq!(headers.to_string(), "MIME-Version: 1.0\r\n"); headers.set(MimeVersion::new(0, 1)); - assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n"); + assert_eq!(headers.to_string(), "MIME-Version: 0.1\r\n"); } #[test] fn parse_mime_version() { let mut headers = Headers::new(); - headers.set_raw("MIME-Version", "1.0"); + headers.set_raw( + HeaderName::new_from_ascii_str("MIME-Version"), + "1.0".to_string(), + ); - assert_eq!(headers.get::(), Some(&MIME_VERSION_1_0)); + assert_eq!(headers.get::(), Some(MIME_VERSION_1_0)); - headers.set_raw("MIME-Version", "0.1"); + headers.set_raw( + HeaderName::new_from_ascii_str("MIME-Version"), + "0.1".to_string(), + ); - assert_eq!(headers.get::(), Some(&MimeVersion::new(0, 1))); + assert_eq!(headers.get::(), Some(MimeVersion::new(0, 1))); } } diff --git a/src/message/header/textual.rs b/src/message/header/textual.rs index 1c4a4c2..be7b660 100644 --- a/src/message/header/textual.rs +++ b/src/message/header/textual.rs @@ -1,34 +1,23 @@ -use crate::message::utf8_b; -use hyperx::{ - header::{Formatter as HeaderFormatter, Header, RawLike}, - Error as HeaderError, Result as HyperResult, -}; -use std::{fmt::Result as FmtResult, str::from_utf8}; +use super::{Header, HeaderName}; +use crate::BoxError; macro_rules! text_header { ($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => { - #[derive(Debug, Clone, PartialEq)] $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] pub struct $type_name(String); impl Header for $type_name { - fn header_name() -> &'static str { - $header_name + fn name() -> HeaderName { + HeaderName::new_from_ascii_str($header_name) } - fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name> - where - T: RawLike<'a>, - Self: Sized, - { - raw.one() - .ok_or(HeaderError::Header) - .and_then(parse_text) - .map($type_name) + fn parse(s: &str) -> Result { + Ok(Self(s.into())) } - fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { - fmt_text(&self.0, f) + fn display(&self) -> String { + self.0.clone() } } @@ -94,30 +83,17 @@ text_header! { Header(ContentLocation, "Content-Location") } -fn parse_text(raw: &[u8]) -> HyperResult { - if let Ok(src) = from_utf8(raw) { - if let Some(txt) = utf8_b::decode(src) { - return Ok(txt); - } - } - Err(HeaderError::Header) -} - -fn fmt_text(s: &str, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { - f.fmt_line(&utf8_b::encode(s)) -} - #[cfg(test)] mod test { use super::Subject; - use hyperx::header::Headers; + use crate::message::header::{HeaderName, Headers}; #[test] fn format_ascii() { let mut headers = Headers::new(); headers.set(Subject("Sample subject".into())); - assert_eq!(format!("{}", headers), "Subject: Sample subject\r\n"); + assert_eq!(headers.to_string(), "Subject: Sample subject\r\n"); } #[test] @@ -126,33 +102,22 @@ mod test { headers.set(Subject("Тема сообщения".into())); assert_eq!( - format!("{}", headers), + headers.to_string(), "Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n" ); } #[test] fn parse_ascii() { - let mut headers = Headers::new(); - headers.set_raw("Subject", "Sample subject"); - - assert_eq!( - headers.get::(), - Some(&Subject("Sample subject".into())) - ); - } - - #[test] - fn parse_utf8() { let mut headers = Headers::new(); headers.set_raw( - "Subject", - "=?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=", + HeaderName::new_from_ascii_str("Subject"), + "Sample subject".to_string(), ); assert_eq!( headers.get::(), - Some(&Subject("Тема сообщения".into())) + Some(Subject("Sample subject".into())) ); } } diff --git a/src/message/mailbox/types.rs b/src/message/mailbox/types.rs index e1a95ee..a08d610 100644 --- a/src/message/mailbox/types.rs +++ b/src/message/mailbox/types.rs @@ -1,7 +1,4 @@ -use crate::{ - address::{Address, AddressError}, - message::utf8_b, -}; +use crate::address::{Address, AddressError}; use std::{ convert::TryFrom, fmt::{Display, Formatter, Result as FmtResult, Write}, @@ -66,14 +63,6 @@ impl Mailbox { pub fn new(name: Option, email: Address) -> Self { Mailbox { name, email } } - - /// Encode addressee name using function - pub(crate) fn recode_name(&self, f: F) -> Self - where - F: FnOnce(&str) -> String, - { - Mailbox::new(self.name.clone().map(|s| f(&s)), self.email.clone()) - } } impl Display for Mailbox { @@ -332,19 +321,7 @@ impl FromStr for Mailboxes { fn from_str(src: &str) -> Result { src.split(',') - .map(|m| { - m.trim().parse().and_then(|Mailbox { name, email }| { - if let Some(name) = name { - if let Some(name) = utf8_b::decode(&name) { - Ok(Mailbox::new(Some(name), email)) - } else { - Err(AddressError::InvalidUtf8b) - } - } else { - Ok(Mailbox::new(None, email)) - } - }) - }) + .map(|m| m.trim().parse()) .collect::, _>>() .map(Mailboxes) } diff --git a/src/message/mimebody.rs b/src/message/mimebody.rs index 545f9c4..e039c7c 100644 --- a/src/message/mimebody.rs +++ b/src/message/mimebody.rs @@ -66,7 +66,7 @@ impl SinglePartBuilder { /// Build singlepart using body pub fn body(mut self, body: T) -> SinglePart { - let maybe_encoding = self.headers.get::().copied(); + let maybe_encoding = self.headers.get::(); let body = body.into_body(maybe_encoding); self.headers.set(body.encoding()); @@ -471,7 +471,7 @@ mod test { #[test] fn multi_part_mixed() { let part = MultiPart::mixed() - .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") + .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .part(Part::Single( SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) @@ -486,27 +486,31 @@ mod test { .body(String::from("int main() { return 0; }")), ); - assert_eq!(String::from_utf8(part.formatted()).unwrap(), - concat!("Content-Type: multipart/mixed;", - " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n", - "\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\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=utf-8\r\n", - "Content-Disposition: attachment; filename=\"example.c\"\r\n", - "Content-Transfer-Encoding: binary\r\n", - "\r\n", - "int main() { return 0; }\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); + assert_eq!( + String::from_utf8(part.formatted()).unwrap(), + concat!( + "Content-Type: multipart/mixed; \r\n", + " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", + "\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", + "Content-Type: text/plain; charset=utf-8\r\n", + "Content-Transfer-Encoding: binary\r\n", + "\r\n", + "Текст письма в уникоде\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\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", + "int main() { return 0; }\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n" + ) + ); } #[test] fn multi_part_encrypted() { let part = MultiPart::encrypted("application/pgp-encrypted".to_owned()) - .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") + .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .part(Part::Single( SinglePart::builder() .header(header::ContentType::parse("application/pgp-encrypted").unwrap()) @@ -529,27 +533,31 @@ mod test { ))), ); - assert_eq!(String::from_utf8(part.formatted()).unwrap(), - concat!("Content-Type: multipart/encrypted;", - " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";", - " protocol=\"application/pgp-encrypted\"\r\n", - "\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", - "Content-Type: application/pgp-encrypted\r\n", - "Content-Transfer-Encoding: 7bit\r\n", - "\r\n", - "Version: 1\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", - "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n", - "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n", - "Content-Transfer-Encoding: 7bit\r\n", - "\r\n", - "-----BEGIN PGP MESSAGE-----\r\n", - "wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n", - "...\r\n", - "-----END PGP MESSAGE-----\r\n", - "\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); + assert_eq!( + String::from_utf8(part.formatted()).unwrap(), + concat!( + "Content-Type: multipart/encrypted; \r\n", + " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n", + " protocol=\"application/pgp-encrypted\"\r\n", + "\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", + "Content-Type: application/pgp-encrypted\r\n", + "Content-Transfer-Encoding: 7bit\r\n", + "\r\n", + "Version: 1\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", + "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n", + "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n", + "Content-Transfer-Encoding: 7bit\r\n", + "\r\n", + "-----BEGIN PGP MESSAGE-----\r\n", + "wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n", + "...\r\n", + "-----END PGP MESSAGE-----\r\n", + "\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n" + ) + ); } #[test] fn multi_part_signed() { @@ -557,7 +565,7 @@ mod test { "application/pgp-signature".to_owned(), "pgp-sha256".to_owned(), ) - .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") + .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .part(Part::Single( SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) @@ -581,37 +589,41 @@ mod test { ))), ); - assert_eq!(String::from_utf8(part.formatted()).unwrap(), - concat!("Content-Type: multipart/signed;", - " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";", - " protocol=\"application/pgp-signature\";", - " micalg=\"pgp-sha256\"\r\n", - "\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\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", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", - "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n", - "Content-Disposition: attachment; filename=\"signature.asc\"\r\n", - "Content-Transfer-Encoding: 7bit\r\n", - "\r\n", - "-----BEGIN PGP SIGNATURE-----\r\n", - "\r\n", - "iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n", - "udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n", - "PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n", - "=3FYZ\r\n", - "-----END PGP SIGNATURE-----\r\n", - "\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); + assert_eq!( + String::from_utf8(part.formatted()).unwrap(), + concat!( + "Content-Type: multipart/signed; \r\n", + " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n", + " protocol=\"application/pgp-signature\";", + " micalg=\"pgp-sha256\"\r\n", + "\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\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", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", + "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n", + "Content-Disposition: attachment; filename=\"signature.asc\"\r\n", + "Content-Transfer-Encoding: 7bit\r\n", + "\r\n", + "-----BEGIN PGP SIGNATURE-----\r\n", + "\r\n", + "iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n", + "udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n", + "PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n", + "=3FYZ\r\n", + "-----END PGP SIGNATURE-----\r\n", + "\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n" + ) + ); } #[test] fn multi_part_alternative() { let part = MultiPart::alternative() - .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") + .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .part(Part::Single(SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .header(header::ContentTransferEncoding::Binary) @@ -622,28 +634,28 @@ mod test { .body(String::from("

Текст письма в уникоде

"))); assert_eq!(String::from_utf8(part.formatted()).unwrap(), - concat!("Content-Type: multipart/alternative;", - " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n", + concat!("Content-Type: multipart/alternative; \r\n", + " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\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", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: text/html; charset=utf-8\r\n", "Content-Transfer-Encoding: binary\r\n", "\r\n", "

Текст письма в уникоде

\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n")); } #[test] fn multi_part_mixed_related() { let part = MultiPart::mixed() - .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") + .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .multipart(MultiPart::related() - .boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh") + .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .singlepart(SinglePart::builder() .header(header::ContentType::TEXT_HTML) .header(header::ContentTransferEncoding::Binary) @@ -660,19 +672,19 @@ mod test { .body(String::from("int main() { return 0; }"))); assert_eq!(String::from_utf8(part.formatted()).unwrap(), - concat!("Content-Type: multipart/mixed;", - " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n", + concat!("Content-Type: multipart/mixed; \r\n", + " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", - "Content-Type: multipart/related;", - " boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", + "Content-Type: multipart/related; \r\n", + " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", - "--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: text/html; charset=utf-8\r\n", "Content-Transfer-Encoding: binary\r\n", "\r\n", "

Текст письма в уникоде

\r\n", - "--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: image/png\r\n", "Content-Location: /image.png\r\n", "Content-Transfer-Encoding: base64\r\n", @@ -680,14 +692,14 @@ mod test { "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n", "ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n", "NTY3ODkwMTIzNDU2Nzg5MA==\r\n", - "--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n", + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\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", "int main() { return 0; }\r\n", - "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); + "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n")); } #[test] diff --git a/src/message/mod.rs b/src/message/mod.rs index 7117fbe..b09e614 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -226,6 +226,8 @@ //! ``` //! +use std::{convert::TryFrom, io::Write, iter, time::SystemTime}; + pub use body::{Body, IntoBody, MaybeString}; pub use mailbox::*; pub use mimebody::*; @@ -234,9 +236,6 @@ mod body; pub mod header; mod mailbox; mod mimebody; -mod utf8_b; - -use std::{convert::TryFrom, io::Write, iter, time::SystemTime}; use crate::{ address::Envelope, @@ -275,12 +274,13 @@ impl MessageBuilder { } /// Add mailbox to header - pub fn mailbox(mut self, header: H) -> Self { - if self.headers.has::() { - self.headers.get_mut::().unwrap().join_mailboxes(header); - self - } else { - self.header(header) + pub fn mailbox(self, header: H) -> Self { + match self.headers.get::() { + Some(mut header_) => { + header_.join_mailboxes(header); + self.header(header_) + } + None => self.header(header), } } @@ -431,7 +431,7 @@ impl MessageBuilder { // Fail is missing correct originator (Sender or From) match res.headers.get::() { Some(header::From(f)) => { - let from: Vec = f.clone().into(); + let from: Vec = f.into(); if from.len() > 1 && res.headers.get::().is_none() { return Err(EmailError::TooManyFrom); } @@ -458,7 +458,7 @@ impl MessageBuilder { /// `Content-Transfer-Encoding`, based on the most efficient and valid encoding /// for `body`. pub fn body(mut self, body: T) -> Result { - let maybe_encoding = self.headers.get::().copied(); + let maybe_encoding = self.headers.get::(); let body = body.into_body(maybe_encoding); self.headers.set(body.encoding()); @@ -643,7 +643,7 @@ mod test { let expected = String::from_utf8(file_expected).unwrap(); for (i, line) in output.lines().zip(expected.lines()).enumerate() { - if i == 6 || i == 8 || i == 13 || i == 232 { + if i == 7 || i == 9 || i == 14 || i == 233 { continue; } diff --git a/src/message/utf8_b.rs b/src/message/utf8_b.rs deleted file mode 100644 index ae6d78e..0000000 --- a/src/message/utf8_b.rs +++ /dev/null @@ -1,60 +0,0 @@ -// https://tools.ietf.org/html/rfc1522 - -fn allowed_char(c: char) -> bool { - c >= 1 as char && c <= 9 as char - || c == 11 as char - || c == 12 as char - || c >= 14 as char && c <= 127 as char -} - -pub fn encode(s: &str) -> String { - if s.chars().all(allowed_char) { - s.into() - } else { - format!("=?utf-8?b?{}?=", base64::encode(s)) - } -} - -pub fn decode(s: &str) -> Option { - s.strip_prefix("=?utf-8?b?") - .and_then(|stripped| stripped.strip_suffix("?=")) - .map_or_else( - || Some(s.into()), - |stripped| { - let decoded = base64::decode(stripped).ok()?; - let decoded = String::from_utf8(decoded).ok()?; - Some(decoded) - }, - ) -} - -#[cfg(test)] -mod test { - use super::{decode, encode}; - - #[test] - fn encode_ascii() { - assert_eq!(&encode("Kayo. ?"), "Kayo. ?"); - } - - #[test] - fn decode_ascii() { - assert_eq!(decode("Kayo. ?"), Some("Kayo. ?".into())); - } - - #[test] - fn encode_utf8() { - assert_eq!( - &encode("Привет, мир!"), - "=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?=" - ); - } - - #[test] - fn decode_utf8() { - assert_eq!( - decode("=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="), - Some("Привет, мир!".into()) - ); - } -} diff --git a/testdata/email_with_png.eml b/testdata/email_with_png.eml index 298e48a..a00c8b4 100644 --- a/testdata/email_with_png.eml +++ b/testdata/email_with_png.eml @@ -4,7 +4,8 @@ Reply-To: Yuin To: Hei Subject: Happy new year MIME-Version: 1.0 -Content-Type: multipart/related; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1" +Content-Type: multipart/related; + boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA" --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 Content-Type: text/html; charset=utf-8