Replace hyperx Header and Headers with our own implementation (#607)

* Replace hyperx Header and Headers with our own implementation

* Remove utf8_b

* Add RFC 1522 encoder

* Fix most tests

* Throw away old tests

* Header encoding tests

* Fix slicing in the middle of a char

* Content-Disposition after rebase

* Fix the rest of the tests

* Fix useless clone clippy warnings

* Remove Headers::get_raw_mut

* HeaderName::new_from_ascii fallible API

* Tidy up HeaderName::new_from_ascii_str

* HeaderName::new_from_ascii(_str) tests
This commit is contained in:
Paolo Barbolini
2021-05-01 13:27:00 +02:00
committed by GitHub
parent 69334fe5eb
commit 31de9e508b
16 changed files with 1048 additions and 496 deletions

View File

@@ -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"]

View File

@@ -112,11 +112,11 @@ impl TryFrom<&Headers> for Envelope {
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
let from = match headers.get::<header::Sender>() {
// 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::<header::From>() {
Some(header::From(a)) => {
let from: Vec<Mailbox> = a.clone().into();
let from: Vec<Mailbox> = 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<Address>,
mailboxes: Option<&Mailboxes>,
mailboxes: Option<Mailboxes>,
) {
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::<header::To>().map(|h| &h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| &h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| &h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| h.0));
Self::new(from, to)
}

View File

@@ -173,11 +173,12 @@ pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
#[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());

View File

@@ -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<Self, BoxError> {
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<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::<ContentTransferEncoding>()
.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::<ContentTransferEncoding>(),
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::<ContentTransferEncoding>(),
Some(ContentTransferEncoding::SevenBit)
);
headers.set_raw(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"base64".to_string(),
);
assert_eq!(
headers.get::<ContentTransferEncoding>(),
Some(&ContentTransferEncoding::Base64)
Some(ContentTransferEncoding::Base64)
);
}
}

View File

@@ -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<Self>
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<Self, BoxError> {
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::<ContentDisposition>(),
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::<ContentDisposition>(),
Some(&ContentDisposition::attachment("something.txt"))
Some(ContentDisposition::attachment("something.txt"))
);
}
}

View File

@@ -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<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 parse(s: &str) -> Result<Self, BoxError> {
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::<ContentType>(), Some(&ContentType::TEXT_PLAIN));
assert_eq!(headers.get::<ContentType>(), 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::<ContentType>(), Some(&ContentType::TEXT_HTML));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
}
}

View File

@@ -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<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| {
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<Self, BoxError> {
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::<HttpDate>()
.map(Self)
.map_err(|_| HeaderError::Header)
})
Ok(Self(s.parse::<HttpDate>()?))
}
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<Date> 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::<Date>(),
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::<Date>(),
Some(&Date::from(
Some(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
))
);

View File

@@ -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<Self> 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<Self, BoxError> {
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<Self, BoxError> {
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<Mailboxes> {
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::<Vec<_>>(),
))
}
#[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. <kayo@example.com>\r\n");
assert_eq!(headers.to_string(), "From: K. <kayo@example.com>\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. <kayo@example.com>, Pony P. <pony@domain.tld>\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=?= <kayo@example.com>\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::<From>(), Some(&From(from)));
assert_eq!(headers.get::<From>(), Some(From(from)));
}
#[test]
@@ -274,9 +243,12 @@ mod test {
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>");
headers.set_raw(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>".to_string(),
);
assert_eq!(headers.get::<From>(), Some(&From(from)));
assert_eq!(headers.get::<From>(), 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::<From>(), Some(&From(from.into())));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
@@ -300,18 +275,11 @@ mod test {
];
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
headers.set_raw(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(),
);
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
}
#[test]
fn parse_single_with_utf8_name() {
let from: Vec<Mailbox> = vec!["Кайо <kayo@example.com>".parse().unwrap()];
let mut headers = Headers::new();
headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>");
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
}

View File

@@ -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<Self, BoxError>;
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<H: Header>(&self) -> Option<H> {
self.get_raw(&H::name()).and_then(|raw| H::parse(raw).ok())
}
pub fn set<H: Header>(&mut self, header: H) {
self.set_raw(H::name(), header.display());
}
pub fn remove<H: Header>(&mut self) -> Option<H> {
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<usize> {
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<Self> {
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<str> for HeaderName {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<HeaderName> 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<HeaderName> 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<Self::Item> {
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 <example@example.com>, Jean Dupont <jean@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
"To: John Doe <example@example.com>, Jean Dupont <jean@example.com>\r\n"
);
}
#[test]
fn format_ascii_with_folding() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("To"),
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
concat!(
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith \r\n",
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand \r\n",
" <jemand@example.com>, Jean Dupont <jean@example.com>\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 <sean@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n"
);
}
#[test]
fn format_special_emoji() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("To"),
"🌎 <world@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
"To: =?utf-8?b?8J+Mjg==?= <world@example.com>\r\n"
);
}
#[test]
fn format_special_with_folding() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("To"),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\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"),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(),
);
headers.insert_raw(
HeaderName::new_from_ascii_str("From"),
"Someone <somewhere@example.com>".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==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
"From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
)
);
}
}

View File

@@ -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<Self, BoxError> {
let mut s = s.split('.');
let major = s
.next()
.expect("The first call to next for a Split<char> 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<Self>
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::<MimeVersion>(), Some(&MIME_VERSION_1_0));
assert_eq!(headers.get::<MimeVersion>(), 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::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
}
}

View File

@@ -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<Self, BoxError> {
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<String> {
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::<Subject>(),
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::<Subject>(),
Some(&Subject("Тема сообщения".into()))
Some(Subject("Sample subject".into()))
);
}
}

View File

@@ -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<String>, email: Address) -> Self {
Mailbox { name, email }
}
/// Encode addressee name using function
pub(crate) fn recode_name<F>(&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<Self, Self::Err> {
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::<Result<Vec<_>, _>>()
.map(Mailboxes)
}

View File

@@ -66,7 +66,7 @@ impl SinglePartBuilder {
/// Build singlepart using body
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
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("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
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",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\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",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\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]

View File

@@ -226,6 +226,8 @@
//! ```
//! </details>
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<H: Header + MailboxesHeader>(mut self, header: H) -> Self {
if self.headers.has::<H>() {
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
self
} else {
self.header(header)
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
match self.headers.get::<H>() {
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::<header::From>() {
Some(header::From(f)) => {
let from: Vec<Mailbox> = f.clone().into();
let from: Vec<Mailbox> = f.into();
if from.len() > 1 && res.headers.get::<header::Sender>().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<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
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;
}

View File

@@ -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<String> {
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())
);
}
}

View File

@@ -4,7 +4,8 @@ Reply-To: Yuin <yuin@domain.tld>
To: Hei <hei@domain.tld>
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