Replace email builder by a new implementation (#393)

* Update dependencies (#386)

* Update dependencies and set MSRV to 1.40

* update hyperx

* Use display instead of description for errors

* Make hostname an optional feature

* Envelope from headers

* Update hyperx to 1.0

* rename builder to message

* Cleanup and make Transport send Messages

* Update rustls from 0.16 to 0.17

* Move transports into a common folder

* Merge imports from same crate

* Add message creation example to the site

* Hide "extern crate" in doc examples

* Add References and In-Reply-To methods

* Add message-id header

* Add blog posts and improve doc examples
This commit is contained in:
Alexis Mousset
2020-04-18 23:10:03 +02:00
committed by GitHub
parent a440ae5c79
commit 53aa5b4df6
63 changed files with 3753 additions and 1513 deletions

282
src/address.rs Normal file
View File

@@ -0,0 +1,282 @@
//! Representation of an email address
use idna::domain_to_ascii;
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
convert::TryFrom,
error::Error,
ffi::OsStr,
fmt::{Display, Formatter, Result as FmtResult},
net::IpAddr,
str::FromStr,
};
/// Email address
///
/// This type contains email in canonical form (_user@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Address {
/// User part
pub user: String,
/// Domain part
pub domain: String,
/// Complete address
complete: String,
}
impl<U, D> TryFrom<(U, D)> for Address
where
U: Into<String>,
D: Into<String>,
{
type Error = AddressError;
fn try_from(from: (U, D)) -> Result<Self, Self::Error> {
let (user, domain) = from;
Self::new(user, domain)
}
}
// Regex from the specs
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
// It will mark esoteric email addresses like quoted string as invalid
static USER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
)
.unwrap()
});
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
impl Address {
/// Create email address from parts
#[inline]
pub fn new<U: Into<String>, D: Into<String>>(user: U, domain: D) -> Result<Self, AddressError> {
let user = user.into();
Address::check_user(&user)?;
let domain = domain.into();
Address::check_domain(&domain)?;
let complete = format!("{}@{}", &user, &domain);
Ok(Address {
user,
domain,
complete,
})
}
pub fn check_user(user: &str) -> Result<(), AddressError> {
if USER_RE.is_match(user) {
Ok(())
} else {
Err(AddressError::InvalidUser)
}
}
pub fn check_domain(domain: &str) -> Result<(), AddressError> {
Address::check_domain_ascii(domain).or_else(|_| {
domain_to_ascii(domain)
.map_err(|_| AddressError::InvalidDomain)
.and_then(|domain| Address::check_domain_ascii(&domain))
})
}
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
if DOMAIN_RE.is_match(domain) {
return Ok(());
}
if let Some(caps) = LITERAL_RE.captures(domain) {
if let Some(cap) = caps.get(1) {
if cap.as_str().parse::<IpAddr>().is_ok() {
return Ok(());
}
}
}
Err(AddressError::InvalidDomain)
}
}
impl Display for Address {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
f.write_str(&self.complete)
}
}
impl FromStr for Address {
type Err = AddressError;
fn from_str(val: &str) -> Result<Self, AddressError> {
if val.is_empty() || !val.contains('@') {
return Err(AddressError::MissingParts);
}
let parts: Vec<&str> = val.rsplitn(2, '@').collect();
let user = parts[1];
let domain = parts[0];
Address::check_user(user)
.and_then(|_| Address::check_domain(domain))
.map(|_| Address {
user: user.into(),
domain: domain.into(),
complete: val.to_string(),
})
}
}
impl AsRef<str> for Address {
fn as_ref(&self) -> &str {
&self.complete.as_ref()
}
}
impl AsRef<OsStr> for Address {
fn as_ref(&self) -> &OsStr {
self.complete.as_ref()
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum AddressError {
MissingParts,
Unbalanced,
InvalidUser,
InvalidDomain,
InvalidUtf8b,
}
impl Error for AddressError {}
impl Display for AddressError {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
AddressError::MissingParts => f.write_str("Missing domain or user"),
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
AddressError::InvalidUser => f.write_str("Invalid email user"),
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
AddressError::InvalidUtf8b => f.write_str("Invalid UTF8b data"),
}
}
}
#[cfg(feature = "serde")]
pub mod serde {
use crate::address::Address;
use serde::{
de::{Deserializer, Error as DeError, MapAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use std::fmt::{Formatter, Result as FmtResult};
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
User,
Domain,
};
const FIELDS: &[&str] = &["user", "domain"];
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
formatter.write_str("'user' or 'domain'")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: DeError,
{
match value {
"user" => Ok(Field::User),
"domain" => Ok(Field::Domain),
_ => Err(DeError::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct AddressVisitor;
impl<'de> Visitor<'de> for AddressVisitor {
type Value = Address;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
formatter.write_str("email address string or object")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut user = None;
let mut domain = None;
while let Some(key) = map.next_key()? {
match key {
Field::User => {
if user.is_some() {
return Err(DeError::duplicate_field("user"));
}
let val = map.next_value()?;
Address::check_user(val).map_err(DeError::custom)?;
user = Some(val);
}
Field::Domain => {
if domain.is_some() {
return Err(DeError::duplicate_field("domain"));
}
let val = map.next_value()?;
Address::check_domain(val).map_err(DeError::custom)?;
domain = Some(val);
}
}
}
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
// FIXME avoid unwrap here
Ok(Address::new(user, domain).unwrap())
}
}
deserializer.deserialize_any(AddressVisitor)
}
}
}

View File

@@ -1,51 +0,0 @@
//! Error and result type for emails
use self::Error::*;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Envelope error
Envelope(crate::error::Error),
/// Unparseable filename for attachment
CannotParseFilename,
/// IO error
Io(io::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str(&match *self {
CannotParseFilename => "Could not parse attachment filename".to_owned(),
Io(ref err) => err.to_string(),
Envelope(ref err) => err.to_string(),
})
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
Envelope(ref err) => Some(err),
Io(ref err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
}
impl From<crate::error::Error> for Error {
fn from(err: crate::error::Error) -> Error {
Error::Envelope(err)
}
}

View File

@@ -1,762 +0,0 @@
use crate::{error::Error as LettreError, Email, EmailAddress, Envelope};
pub use email::{Address, Header, Mailbox as OriginalMailbox, MimeMessage, MimeMultipartType};
use error::Error;
pub use mime;
use mime::Mime;
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fmt;
use std::fs;
use std::path::Path;
use std::str::FromStr;
use time::OffsetDateTime;
use uuid::Uuid;
pub mod error;
const DT_RFC822Z: &str = "%a, %d %b %Y %T %z";
// From rust-email, allows adding rfc2047 encoding
/// Represents an RFC 5322 mailbox
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Mailbox {
inner: OriginalMailbox,
}
impl Mailbox {
/// Create a new Mailbox without a display name
pub fn new(address: String) -> Mailbox {
Mailbox {
inner: OriginalMailbox::new(address),
}
}
/// Create a new Mailbox with a display name
pub fn new_with_name(name: String, address: String) -> Mailbox {
Mailbox {
inner: OriginalMailbox::new_with_name(encode_rfc2047(&name).to_string(), address),
}
}
fn original(self) -> OriginalMailbox {
self.inner
}
}
impl fmt::Display for Mailbox {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "{}", self.inner)
}
}
impl<'a> From<&'a str> for Mailbox {
fn from(mailbox: &'a str) -> Mailbox {
Mailbox::new(mailbox.into())
}
}
impl From<String> for Mailbox {
fn from(mailbox: String) -> Mailbox {
Mailbox::new(mailbox)
}
}
impl<S: Into<String>, T: Into<String>> From<(S, T)> for Mailbox {
fn from(header: (S, T)) -> Mailbox {
let (address, alias) = header;
Mailbox::new_with_name(alias.into(), address.into())
}
}
/// Encode a UTF-8 string according to RFC 2047, if need be.
///
/// Currently, this only uses "B" encoding, when pure ASCII cannot represent the
/// string accurately.
///
/// Can be used on header content.
pub fn encode_rfc2047(text: &str) -> Cow<str> {
if text.is_ascii() {
Cow::Borrowed(text)
} else {
Cow::Owned(
base64::encode_config(text.as_bytes(), base64::STANDARD)
// base64 so ascii
.as_bytes()
// Max length - wrapping chars
.chunks(75 - 12)
.map(|d| format!("=?utf-8?B?{}?=", std::str::from_utf8(d).unwrap()))
.collect::<Vec<String>>()
.join("\r\n"),
)
}
}
impl From<EmailAddress> for OriginalMailbox {
fn from(addr: EmailAddress) -> Self {
OriginalMailbox::new(addr.into_inner())
}
}
/// Builds a `MimeMessage` structure
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct PartBuilder {
/// Message
message: MimeMessage,
}
impl Default for PartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Represents a message id
pub type MessageId = String;
/// Builds an `Email` structure
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct EmailBuilder {
/// Message
message: PartBuilder,
/// The recipients' addresses for the mail header
to: Vec<Address>,
/// The sender addresses for the mail header
from: Vec<Address>,
/// The Cc addresses for the mail header
cc: Vec<Address>,
/// The Bcc addresses for the mail header
bcc: Vec<Address>,
/// The Reply-To addresses for the mail header
reply_to: Vec<Address>,
/// The In-Reply-To ids for the mail header
in_reply_to: Vec<MessageId>,
/// The References ids for the mail header
references: Vec<MessageId>,
/// The sender address for the mail header
sender: Option<OriginalMailbox>,
/// The envelope
envelope: Option<Envelope>,
/// Date issued
date_issued: bool,
/// Message-ID
message_id: Option<String>,
}
impl PartBuilder {
/// Creates a new empty part
pub fn new() -> PartBuilder {
PartBuilder {
message: MimeMessage::new_blank_message(),
}
}
/// Adds a generic header
pub fn header<A: Into<Header>>(mut self, header: A) -> PartBuilder {
self.message.headers.insert(header.into());
self
}
/// Sets the body
pub fn body<S: Into<String>>(mut self, body: S) -> PartBuilder {
self.message.body = body.into();
self
}
/// Defines a `MimeMultipartType` value
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
self.message.message_type = Some(mime_type);
self
}
/// Adds a `ContentType` header with the given MIME type
pub fn content_type(self, content_type: &Mime) -> PartBuilder {
self.header(("Content-Type", content_type.to_string()))
}
/// Adds a child part
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
self.message.children.push(child);
self
}
/// Gets built `MimeMessage`
pub fn build(mut self) -> MimeMessage {
self.message.update_headers();
self.message
}
}
impl EmailBuilder {
/// Creates a new empty email
pub fn new() -> EmailBuilder {
EmailBuilder {
message: PartBuilder::new(),
to: vec![],
from: vec![],
cc: vec![],
bcc: vec![],
reply_to: vec![],
in_reply_to: vec![],
references: vec![],
sender: None,
envelope: None,
date_issued: false,
message_id: None,
}
}
/// Sets the email body
pub fn body<S: Into<String>>(mut self, body: S) -> EmailBuilder {
self.message = self.message.body(body);
self
}
/// Add a generic header
pub fn header<A: Into<Header>>(mut self, header: A) -> EmailBuilder {
self.message = self.message.header(header);
self
}
/// Adds a `From` header and stores the sender address
pub fn from<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.from.push(Address::Mailbox(mailbox.original()));
self
}
/// Adds a `To` header and stores the recipient address
pub fn to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.to.push(Address::Mailbox(mailbox.original()));
self
}
/// Adds a `Cc` header and stores the recipient address
pub fn cc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.cc.push(Address::Mailbox(mailbox.original()));
self
}
/// Adds a `Bcc` header and stores the recipient address
pub fn bcc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.bcc.push(Address::Mailbox(mailbox.original()));
self
}
/// Adds a `Reply-To` header
pub fn reply_to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.reply_to.push(Address::Mailbox(mailbox.original()));
self
}
/// Adds a `In-Reply-To` header
pub fn in_reply_to(mut self, message_id: MessageId) -> EmailBuilder {
self.in_reply_to.push(message_id);
self
}
/// Adds a `References` header
pub fn references(mut self, message_id: MessageId) -> EmailBuilder {
self.references.push(message_id);
self
}
/// Adds a `Sender` header
pub fn sender<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.sender = Some(mailbox.original());
self
}
/// Adds a `Subject` header
pub fn subject<S: Into<String>>(mut self, subject: S) -> EmailBuilder {
self.message = self.message.header((
"Subject".to_string(),
encode_rfc2047(subject.into().as_ref()),
));
self
}
/// Adds a `Date` header with the given date
pub fn date(mut self, date: &OffsetDateTime) -> EmailBuilder {
self.message = self.message.header(("Date", date.format(DT_RFC822Z)));
self.date_issued = true;
self
}
/// Adds an attachment to the email from a file
///
/// If not specified, the filename will be extracted from the file path.
pub fn attachment_from_file(
self,
path: &Path,
filename: Option<&str>,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
self.attachment(
fs::read(path)?.as_slice(),
filename.unwrap_or(
path.file_name()
.and_then(OsStr::to_str)
.ok_or(Error::CannotParseFilename)?,
),
content_type,
)
}
/// Adds an attachment to the email from a vector of bytes.
pub fn attachment(
self,
body: &[u8],
filename: &str,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
let encoded_body = base64::encode(&body);
let content = PartBuilder::new()
.body(encoded_body)
.header((
"Content-Disposition",
format!("attachment; filename=\"{}\"", filename),
))
.header(("Content-Type", content_type.to_string()))
.header(("Content-Transfer-Encoding", "base64"))
.build();
Ok(self.message_type(MimeMultipartType::Mixed).child(content))
}
/// Embed file so it can be referenced by Content-ID
///
/// If not specified, the filename will be extracted from the file path.
pub fn embed_from_file(
self,
path: &Path,
filename: Option<&str>,
content_type: &Mime,
content_id: &str,
) -> Result<EmailBuilder, Error> {
self.embed(
fs::read(path)?.as_slice(),
filename.unwrap_or(
path.file_name()
.and_then(OsStr::to_str)
.ok_or(Error::CannotParseFilename)?,
),
content_type,
content_id,
)
}
/// Adds an embed to the email from a vector of bytes.
pub fn embed(
self,
body: &[u8],
filename: &str,
content_type: &Mime,
content_id: &str,
) -> Result<EmailBuilder, Error> {
let encoded_body = base64::encode(&body);
let content = PartBuilder::new()
.body(encoded_body)
.header((
"Content-Disposition",
format!("inline; filename=\"{}\"", filename),
))
.header((
"Content-Type",
format!("{}; name=\"{}\"", content_type, filename),
))
.header(("Content-Transfer-Encoding", "base64"))
.header(("Content-ID", format!("<{}>", content_id)))
.build();
Ok(self.message_type(MimeMultipartType::Mixed).child(content))
}
/// Set the message type
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
self.message = self.message.message_type(message_type);
self
}
/// Adds a child
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
self.message = self.message.child(child);
self
}
/// Sets the email body to plain text content
pub fn text<S: Into<String>>(self, body: S) -> EmailBuilder {
let text = PartBuilder::new()
.body(body)
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
.build();
self.child(text)
}
/// Sets the email body to HTML content
pub fn html<S: Into<String>>(self, body: S) -> EmailBuilder {
let html = PartBuilder::new()
.body(body)
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
.build();
self.child(html)
}
/// Sets the email content
pub fn alternative<S: Into<String>, T: Into<String>>(
self,
body_html: S,
body_text: T,
) -> EmailBuilder {
let text = PartBuilder::new()
.body(body_text)
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
.build();
let html = PartBuilder::new()
.body(body_html)
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
.build();
let alternate = PartBuilder::new()
.message_type(MimeMultipartType::Alternative)
.child(text)
.child(html);
self.message_type(MimeMultipartType::Mixed)
.child(alternate.build())
}
/// Sets the `Message-ID` header
pub fn message_id<S: Clone + Into<String>>(mut self, id: S) -> EmailBuilder {
self.message = self.message.header(("Message-ID", id.clone()));
self.message_id = Some(id.into());
self
}
/// Sets the envelope for manual destination control
/// If this function is not called, the envelope will be calculated
/// from the "to" and "cc" addresses you set.
pub fn envelope(mut self, envelope: Envelope) -> EmailBuilder {
self.envelope = Some(envelope);
self
}
/// Only builds the body, this can be used to encrypt or sign
/// using S/MIME
pub fn build_body(self) -> Result<Vec<u8>, Error> {
Ok(self.message.build().as_string().into_bytes())
}
/// Builds the Email
pub fn build(mut self) -> Result<Email, Error> {
// If there are multiple addresses in "From", the "Sender" is required.
if self.from.len() >= 2 && self.sender.is_none() {
// So, we must find something to put as Sender.
for possible_sender in &self.from {
// Only a mailbox can be used as sender, not Address::Group.
if let Address::Mailbox(ref mbx) = *possible_sender {
self.sender = Some(mbx.clone());
break;
}
}
// Address::Group is not yet supported, so the line below will never panic.
// If groups are supported one day, add another Error for this case
// and return it here, if sender_header is still None at this point.
assert!(self.sender.is_some());
}
// Add the sender header, if any.
if let Some(ref v) = self.sender {
self.message = self.message.header(("Sender", v.to_string()));
}
// Calculate the envelope
let envelope = match self.envelope {
Some(e) => e,
None => {
// we need to generate the envelope
let mut to = vec![];
// add all receivers in to_header and cc_header
for receiver in self.to.iter().chain(self.cc.iter()).chain(self.bcc.iter()) {
match *receiver {
Address::Mailbox(ref m) => to.push(EmailAddress::from_str(&m.address)?),
Address::Group(_, ref ms) => {
for m in ms.iter() {
to.push(EmailAddress::from_str(&m.address.clone())?);
}
}
}
}
let from = Some(EmailAddress::from_str(&match self.sender {
Some(x) => Ok(x.address), // if we have a sender_header, use it
None => {
// use a from header
debug_assert!(self.from.len() <= 1); // else we'd have sender_header
match self.from.first() {
Some(a) => match *a {
// if we have a from header
Address::Mailbox(ref mailbox) => Ok(mailbox.address.clone()), // use it
Address::Group(_, ref mailbox_list) => match mailbox_list.first() {
// if it's an author group, use the first author
Some(mailbox) => Ok(mailbox.address.clone()),
// for an empty author group (the rarest of the rare cases)
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
},
},
// if we don't have a from header
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
}
}
}?)?);
Envelope::new(from, to)?
}
};
// Add the collected addresses as mailbox-list all at once.
// The unwraps are fine because the conversions for Vec<Address> never errs.
if !self.to.is_empty() {
self.message = self
.message
.header(Header::new_with_value("To".into(), self.to).unwrap());
}
if !self.from.is_empty() {
self.message = self
.message
.header(Header::new_with_value("From".into(), self.from).unwrap());
} else if let Some(from) = envelope.from() {
let from = vec![Address::new_mailbox(from.to_string())];
self.message = self
.message
.header(Header::new_with_value("From".into(), from).unwrap());
} else {
return Err(Error::Envelope(LettreError::MissingFrom));
}
if !self.cc.is_empty() {
self.message = self
.message
.header(Header::new_with_value("Cc".into(), self.cc).unwrap());
}
if !self.reply_to.is_empty() {
self.message = self
.message
.header(Header::new_with_value("Reply-To".into(), self.reply_to).unwrap());
}
if !self.in_reply_to.is_empty() {
self.message = self.message.header(
Header::new_with_value("In-Reply-To".into(), self.in_reply_to.join(" ")).unwrap(),
);
}
if !self.references.is_empty() {
self.message = self.message.header(
Header::new_with_value("References".into(), self.references.join(" ")).unwrap(),
);
}
if !self.date_issued {
self.message = self
.message
.header(("Date", OffsetDateTime::now().format(DT_RFC822Z)));
}
self.message = self.message.header(("MIME-Version", "1.0"));
let message_id = match self.message_id {
Some(id) => id,
None => {
let message_id = Uuid::new_v4();
self.message = self
.message
.header(("Message-ID", format!("<{}.lettre@localhost>", message_id)));
message_id.to_string()
}
};
Ok(Email::new(
envelope,
message_id,
self.message.build().as_string().into_bytes(),
))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::EmailAddress;
use time::OffsetDateTime;
#[test]
fn test_encode_rfc2047() {
assert_eq!(encode_rfc2047("test"), "test");
assert_eq!(encode_rfc2047("testà"), "=?utf-8?B?dGVzdMOg?=");
assert_eq!(
encode_rfc2047(
"testàtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest"
),
"=?utf-8?B?dGVzdMOgdGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHR?=\r\n=?utf-8?B?lc3R0ZXN0dGVzdHRlc3R0ZXN0dGVzdHRlc3R0ZXN0?="
);
}
#[test]
fn test_multiple_from() {
let email_builder = EmailBuilder::new();
let date_now = OffsetDateTime::now();
let email: Email = email_builder
.to("anna@example.com")
.from("dieter@example.com")
.from("joachim@example.com")
.date(&date_now)
.subject("Invitation")
.body("We invite you!")
.build()
.unwrap()
.into();
let id = email.message_id().to_string();
assert_eq!(
email.message_to_string().unwrap(),
format!(
"Date: {}\r\nSubject: Invitation\r\nSender: \
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
date_now.format(DT_RFC822Z),
id
)
);
}
#[test]
fn test_email_builder() {
let email_builder = EmailBuilder::new();
let date_now = OffsetDateTime::now();
let email: Email = email_builder
.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.cc(("cc2@localhost", "Aliäs"))
.bcc("bcc@localhost")
.reply_to("reply@localhost")
.in_reply_to("original".to_string())
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap()
.into();
let id = email.message_id().to_string();
assert_eq!(
email.message_to_string().unwrap(),
format!(
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>, \"=?utf-8?B?QWxpw6Rz?=\" <cc2@localhost>\r\n\
Reply-To: <reply@localhost>\r\nIn-Reply-To: original\r\n\
MIME-Version: 1.0\r\nMessage-ID: \
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
date_now.format(DT_RFC822Z),
id
)
);
}
#[test]
fn test_custom_message_id() {
let email_builder = EmailBuilder::new();
let date_now = OffsetDateTime::now();
let email: Email = email_builder
.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.bcc("bcc@localhost")
.reply_to("reply@localhost")
.in_reply_to("original".to_string())
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.message_id("my-shiny-id")
.build()
.unwrap()
.into();
assert_eq!(
email.message_to_string().unwrap(),
format!(
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nMessage-ID: \
my-shiny-id\r\nSender: <sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
<reply@localhost>\r\nIn-Reply-To: original\r\nMIME-Version: 1.0\r\n\r\nHello \
World!\r\n",
date_now.format(DT_RFC822Z)
)
);
}
#[test]
fn test_email_builder_body() {
let date_now = OffsetDateTime::now();
let email_builder = EmailBuilder::new()
.text("TestTest")
.subject("A Subject")
.to("user@localhost")
.date(&date_now);
let string_res = String::from_utf8(email_builder.build_body().unwrap());
assert!(string_res.unwrap().starts_with("Subject: A Subject\r\n"));
}
#[test]
fn test_email_subject_encoding() {
let date_now = OffsetDateTime::now();
let email_builder = EmailBuilder::new()
.text("TestTest")
.subject("A ö Subject")
.to("user@localhost")
.date(&date_now);
let string_res = String::from_utf8(email_builder.build_body().unwrap());
assert!(string_res
.unwrap()
.starts_with("Subject: =?utf-8?B?QSDDtiBTdWJqZWN0?=\r\n"));
}
#[test]
fn test_email_sendable() {
let email_builder = EmailBuilder::new();
let date_now = OffsetDateTime::now();
let email: Email = email_builder
.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.bcc("bcc@localhost")
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap()
.into();
assert_eq!(
email.envelope().from().unwrap().to_string(),
"sender@localhost".to_string()
);
assert_eq!(
email.envelope().to(),
vec![
EmailAddress::new("user@localhost".to_string()).unwrap(),
EmailAddress::new("cc@localhost".to_string()).unwrap(),
EmailAddress::new("bcc@localhost".to_string()).unwrap(),
]
.as_slice()
);
}
}

View File

@@ -1,35 +1,52 @@
use self::Error::*;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// Error type for email content
#[derive(Debug, Clone, Copy)]
#[derive(Debug)]
pub enum Error {
/// Missing from in envelope
MissingFrom,
/// Missing to in envelope
MissingTo,
/// Invalid email
InvalidEmailAddress,
/// Can only be one from in envelope
TooManyFrom,
/// Invalid email: missing at
EmailMissingAt,
/// Invalid email: missing local part
EmailMissingLocalPart,
/// Invalid email: missing domain
EmailMissingDomain,
/// Cannot parse filename for attachment
CannotParseFilename,
/// IO error
Io(std::io::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str(&match *self {
MissingFrom => "missing source address, invalid envelope".to_owned(),
MissingTo => "missing destination address, invalid envelope".to_owned(),
InvalidEmailAddress => "invalid email address".to_owned(),
fmt.write_str(&match self {
Error::MissingFrom => "missing source address, invalid envelope".to_string(),
Error::MissingTo => "missing destination address, invalid envelope".to_string(),
Error::TooManyFrom => "there can only be one source address".to_string(),
Error::EmailMissingAt => "missing @ in email address".to_string(),
Error::EmailMissingLocalPart => "missing local part in email address".to_string(),
Error::EmailMissingDomain => "missing domain in email address".to_string(),
Error::CannotParseFilename => "could not parse attachment filename".to_string(),
Error::Io(e) => e.to_string(),
})
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
None
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::Io(err)
}
}
/// Email result type
pub type EmailResult<T> = Result<T, Error>;
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
None
}
}

View File

@@ -1,98 +1,43 @@
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
//!
//! This mailer contains the available transports for your emails.
//! Lettre provides an email builder and several email transports.
//!
#![doc(html_root_url = "https://docs.rs/lettre/0.10.0")]
#![doc(html_favicon_url = "https://lettre.at/favicon.png")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![deny(
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces
)]
#[cfg(feature = "builder")]
pub mod builder;
pub mod address;
pub mod error;
#[cfg(feature = "file-transport")]
pub mod file;
#[cfg(feature = "sendmail-transport")]
pub mod sendmail;
#[cfg(feature = "smtp-transport")]
pub mod smtp;
pub mod stub;
#[cfg(feature = "builder")]
use crate::builder::EmailBuilder;
use crate::error::EmailResult;
pub mod message;
pub mod transport;
pub use crate::address::Address;
use crate::error::Error;
#[cfg(feature = "builder")]
pub use crate::message::{
header::{self, Headers},
Mailboxes, Message,
};
#[cfg(feature = "file-transport")]
pub use crate::file::FileTransport;
pub use crate::transport::file::FileTransport;
#[cfg(feature = "sendmail-transport")]
pub use crate::sendmail::SendmailTransport;
pub use crate::transport::sendmail::SendmailTransport;
#[cfg(feature = "smtp-transport")]
pub use crate::smtp::client::net::ClientTlsParameters;
pub use crate::transport::smtp::client::net::ClientTlsParameters;
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
pub use crate::smtp::r2d2::SmtpConnectionManager;
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
#[cfg(feature = "smtp-transport")]
pub use crate::smtp::{ClientSecurity, SmtpClient, SmtpTransport};
use fast_chemail::is_valid_email;
use std::ffi::OsStr;
use std::fmt::{self, Display, Formatter};
use std::io;
use std::io::Cursor;
use std::io::Read;
use std::str::FromStr;
/// Email address
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EmailAddress(String);
impl EmailAddress {
pub fn new(address: String) -> EmailResult<EmailAddress> {
if !EmailAddress::is_valid(&address) {
return Err(Error::InvalidEmailAddress);
}
Ok(EmailAddress(address))
}
pub fn is_valid(addr: &str) -> bool {
is_valid_email(addr) || addr.ends_with("localhost")
}
pub fn into_inner(self) -> String {
self.0
}
}
impl FromStr for EmailAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
EmailAddress::new(s.to_string())
}
}
impl Display for EmailAddress {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for EmailAddress {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<OsStr> for EmailAddress {
fn as_ref(&self) -> &OsStr {
self.0.as_ref()
}
}
pub use crate::transport::smtp::{ClientSecurity, SmtpClient, SmtpTransport};
#[cfg(feature = "builder")]
use std::convert::TryFrom;
use std::fmt::Display;
/// Simple email envelope representation
///
@@ -103,14 +48,14 @@ pub struct Envelope {
/// The envelope recipients' addresses
///
/// This can not be empty.
forward_path: Vec<EmailAddress>,
forward_path: Vec<Address>,
/// The envelope sender address
reverse_path: Option<EmailAddress>,
reverse_path: Option<Address>,
}
impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty.
pub fn new(from: Option<EmailAddress>, to: Vec<EmailAddress>) -> EmailResult<Envelope> {
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
if to.is_empty() {
return Err(Error::MissingTo);
}
@@ -121,88 +66,128 @@ impl Envelope {
}
/// Destination addresses of the envelope
pub fn to(&self) -> &[EmailAddress] {
pub fn to(&self) -> &[Address] {
self.forward_path.as_slice()
}
/// Source address of the envelope
pub fn from(&self) -> Option<&EmailAddress> {
pub fn from(&self) -> Option<&Address> {
self.reverse_path.as_ref()
}
}
pub enum Message {
Reader(Box<dyn Read + Send>),
Bytes(Cursor<Vec<u8>>),
}
impl TryFrom<&Headers> for Envelope {
type Error = Error;
impl Read for Message {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
Message::Reader(ref mut rdr) => rdr.read(buf),
Message::Bytes(ref mut rdr) => rdr.read(buf),
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
let from = match headers.get::<header::Sender>() {
// If there is a Sender, use it
Some(header::Sender(a)) => Some(a.email.clone()),
// ... else use the first From address
None => match headers.get::<header::From>() {
Some(header::From(ref a)) => Some(a.iter().next().unwrap().email.clone()),
None => None,
},
};
fn add_addresses_from_mailboxes(
addresses: &mut Vec<Address>,
mailboxes: Option<&Mailboxes>,
) {
if let Some(mailboxes) = mailboxes {
for mailbox in mailboxes.iter() {
addresses.push(mailbox.email.clone());
}
}
}
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));
Self::new(from, to)
}
}
/// Sendable email structure
pub struct Email {
envelope: Envelope,
message_id: String,
message: Message,
}
impl Email {
/// Creates a new email builder
#[cfg(feature = "builder")]
pub fn builder() -> EmailBuilder {
EmailBuilder::new()
}
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> Email {
Email {
envelope,
message_id,
message: Message::Bytes(Cursor::new(message)),
}
}
pub fn new_with_reader(
envelope: Envelope,
message_id: String,
message: Box<dyn Read + Send>,
) -> Email {
Email {
envelope,
message_id,
message: Message::Reader(message),
}
}
pub fn envelope(&self) -> &Envelope {
&self.envelope
}
pub fn message_id(&self) -> &str {
&self.message_id
}
pub fn message(self) -> Message {
self.message
}
pub fn message_to_string(mut self) -> Result<String, io::Error> {
let mut message_content = String::new();
self.message.read_to_string(&mut message_content)?;
Ok(message_content)
}
}
// FIXME generate random log id
/// Transport method for emails
pub trait Transport<'a> {
pub trait Transport<'a, B> {
/// Result type for the transport
type Result;
/// Sends the email
fn send<E: Into<Email>>(&mut self, email: E) -> Self::Result;
/// FIXME not mut
fn send(&mut self, email: Message<B>) -> Self::Result
where
B: Display;
/*
{
&mut self,
Box::new(Cursor::new(email.to_string().as_bytes())),
email.envelope(),
Uuid::new_v4().to_string(),
}*/
// TODO allow sending generic data
}
#[cfg(test)]
mod test {
use super::*;
use crate::message::{header, Mailbox, Mailboxes};
use hyperx::header::Headers;
#[test]
fn envelope_from_headers() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From(from));
headers.set(header::To(to));
assert_eq!(
Envelope::try_from(&headers).unwrap(),
Envelope::new(
Some(Address::new("kayo", "example.com").unwrap()),
vec![Address::new("amousset", "example.com").unwrap()]
)
.unwrap()
);
}
#[test]
fn envelope_from_headers_sender() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From(from));
headers.set(header::Sender(sender));
headers.set(header::To(to));
assert_eq!(
Envelope::try_from(&headers).unwrap(),
Envelope::new(
Some(Address::new("kayo2", "example.com").unwrap()),
vec![Address::new("amousset", "example.com").unwrap()]
)
.unwrap()
);
}
#[test]
fn envelope_from_headers_no_to() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From(from));
headers.set(header::Sender(sender));
assert!(Envelope::try_from(&headers).is_err(),);
}
}

451
src/message/encoder.rs Normal file
View File

@@ -0,0 +1,451 @@
use crate::message::header::ContentTransferEncoding;
use bytes::{Buf, BufMut, Bytes, BytesMut, IntoBuf};
use std::{
cmp::min,
error::Error,
fmt::{Debug, Display, Formatter, Result as FmtResult},
};
/// Content encoding error
#[derive(Debug, Clone)]
pub enum EncoderError<E> {
Source(E),
Coding,
}
impl<E> Error for EncoderError<E> where E: Debug + Display {}
impl<E> Display for EncoderError<E>
where
E: Display,
{
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
EncoderError::Source(error) => write!(f, "Source error: {}", error),
EncoderError::Coding => f.write_str("Coding error"),
}
}
}
/// Encoder trait
pub trait EncoderCodec: Send {
/// Encode chunk of data
fn encode_chunk(&mut self, input: &dyn Buf) -> Result<Bytes, ()>;
/// Encode end of stream
///
/// This proposed to use for stateful encoders like *base64*.
fn finish_chunk(&mut self) -> Result<Bytes, ()> {
Ok(Bytes::new())
}
/// Encode all data
fn encode_all(&mut self, source: &dyn Buf) -> Result<Bytes, ()> {
let chunk = self.encode_chunk(source)?;
let end = self.finish_chunk()?;
Ok(if end.is_empty() {
chunk
} else {
let mut chunk = chunk.try_mut().unwrap();
chunk.put(end);
chunk.freeze()
})
}
}
/// 7bit codec
///
struct SevenBitCodec {
line_wrapper: EightBitCodec,
}
impl SevenBitCodec {
pub fn new() -> Self {
SevenBitCodec {
line_wrapper: EightBitCodec::new(),
}
}
}
impl EncoderCodec for SevenBitCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
if chunk.bytes().iter().all(u8::is_ascii) {
self.line_wrapper.encode_chunk(chunk)
} else {
Err(())
}
}
}
/// Quoted-Printable codec
///
struct QuotedPrintableCodec();
impl QuotedPrintableCodec {
pub fn new() -> Self {
QuotedPrintableCodec()
}
}
impl EncoderCodec for QuotedPrintableCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
Ok(quoted_printable::encode(chunk.bytes()).into())
}
}
/// Base64 codec
///
struct Base64Codec {
line_wrapper: EightBitCodec,
last_padding: Bytes,
}
impl Base64Codec {
pub fn new() -> Self {
Base64Codec {
line_wrapper: EightBitCodec::new().with_limit(78 - 2),
last_padding: Bytes::new(),
}
}
}
impl EncoderCodec for Base64Codec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
let in_len = self.last_padding.len() + chunk.remaining();
let out_len = in_len * 4 / 3;
let mut out = BytesMut::with_capacity(out_len);
let chunk = if self.last_padding.is_empty() {
chunk.bytes()[..].into_buf()
} else {
let mut src = BytesMut::with_capacity(3);
let len = min(chunk.remaining(), 3 - self.last_padding.len());
src.put(&self.last_padding);
src.put(&chunk.bytes()[..len]);
// encode beginning
unsafe {
let len = base64::encode_config_slice(&src, base64::STANDARD, out.bytes_mut());
out.advance_mut(len);
}
chunk.bytes()[len..].into_buf()
};
let len = chunk.remaining() - (chunk.remaining() % 3);
let chunk = if len > 0 {
// encode chunk
unsafe {
let len = base64::encode_config_slice(
&chunk.bytes()[..len],
base64::STANDARD,
out.bytes_mut(),
);
out.advance_mut(len);
}
chunk.bytes()[len..].into_buf()
} else {
chunk.bytes()[..].into_buf()
};
// update last padding
self.last_padding = chunk.bytes().into();
self.line_wrapper.encode_chunk(&out.freeze().into_buf())
}
fn finish_chunk(&mut self) -> Result<Bytes, ()> {
let mut out = BytesMut::with_capacity(4);
unsafe {
let len =
base64::encode_config_slice(&self.last_padding, base64::STANDARD, out.bytes_mut());
out.advance_mut(len);
}
self.line_wrapper.encode_chunk(&out.freeze().into_buf())
}
}
/// 8bit codec
///
struct EightBitCodec {
max_length: usize,
line_bytes: usize,
}
const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
impl EightBitCodec {
pub fn new() -> Self {
EightBitCodec {
max_length: DEFAULT_MAX_LINE_LENGTH,
line_bytes: 0,
}
}
pub fn with_limit(mut self, max_length: usize) -> Self {
self.max_length = max_length;
self
}
}
impl EncoderCodec for EightBitCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
let mut out = BytesMut::with_capacity(chunk.remaining() + 20);
let mut src = chunk.bytes()[..].into_buf();
while src.has_remaining() {
let line_break = src.bytes().iter().position(|b| *b == b'\n');
let mut split_pos = if let Some(line_break) = line_break {
line_break
} else {
src.remaining()
};
let max_length = self.max_length - self.line_bytes;
if split_pos < max_length {
// advance line bytes
self.line_bytes += split_pos;
} else {
split_pos = max_length;
// reset line bytes
self.line_bytes = 0;
};
let has_remaining = split_pos < src.remaining();
//let mut taken = src.take(split_pos);
out.reserve(split_pos + if has_remaining { 2 } else { 0 });
//out.put(&mut taken);
out.put(&src.bytes()[..split_pos]);
if has_remaining {
out.put_slice(b"\r\n");
}
src.advance(split_pos);
//src = taken.into_inner();
}
Ok(out.freeze())
}
}
/// Binary codec
///
struct BinaryCodec;
impl BinaryCodec {
pub fn new() -> Self {
BinaryCodec
}
}
impl EncoderCodec for BinaryCodec {
fn encode_chunk(&mut self, chunk: &dyn Buf) -> Result<Bytes, ()> {
Ok(chunk.bytes().into())
}
}
pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec> {
use self::ContentTransferEncoding::*;
if let Some(encoding) = encoding {
match encoding {
SevenBit => Box::new(SevenBitCodec::new()),
QuotedPrintable => Box::new(QuotedPrintableCodec::new()),
Base64 => Box::new(Base64Codec::new()),
EightBit => Box::new(EightBitCodec::new()),
Binary => Box::new(BinaryCodec::new()),
}
} else {
Box::new(BinaryCodec::new())
}
}
#[cfg(test)]
mod test {
use super::{
Base64Codec, BinaryCodec, EightBitCodec, EncoderCodec, QuotedPrintableCodec, SevenBitCodec,
};
use bytes::IntoBuf;
use std::str::from_utf8;
#[test]
fn seven_bit_encode() {
let mut c = SevenBitCodec::new();
assert_eq!(
c.encode_chunk(&"Hello, world!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, world!".into()))
);
assert_eq!(
c.encode_chunk(&"Hello, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Err(())
);
}
#[test]
fn quoted_printable_encode() {
let mut c = QuotedPrintableCodec::new();
assert_eq!(
c.encode_chunk(&"Привет, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok(
"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".into()
))
);
assert_eq!(c.encode_chunk(&"Текст письма в уникоде".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5".into())));
}
#[test]
fn base64_encode() {
let mut c = Base64Codec::new();
assert_eq!(
c.encode_all(&"Привет, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("0J/RgNC40LLQtdGCLCDQvNC40YAh".into()))
);
assert_eq!(
c.encode_all(&"Текст письма в уникоде подлиннее.".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok(concat!(
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ\r\n",
"vtC00LUg0L/QvtC00LvQuNC90L3QtdC1Lg=="
)
.into()))
);
}
#[test]
fn base64_encode_all() {
let mut c = Base64Codec::new();
assert_eq!(
c.encode_all(
&"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую."
.into_buf()
).map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok(
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=").into()
))
);
let mut c = Base64Codec::new();
assert_eq!(
c.encode_all(
&"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это."
.into_buf()
).map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok(
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRjiDRjdGC\r\n",
"0L4u").into()
))
);
}
#[test]
fn base64_encode_chunked() {
let mut c = Base64Codec::new();
assert_eq!(
c.encode_chunk(&"Chunk.".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Q2h1bmsu".into()))
);
assert_eq!(
c.finish_chunk()
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("".into()))
);
let mut c = Base64Codec::new();
assert_eq!(
c.encode_chunk(&"Chunk".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Q2h1".into()))
);
assert_eq!(
c.finish_chunk()
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("bms=".into()))
);
let mut c = Base64Codec::new();
assert_eq!(
c.encode_chunk(&"Chun".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Q2h1".into()))
);
assert_eq!(
c.finish_chunk()
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("bg==".into()))
);
let mut c = Base64Codec::new();
assert_eq!(
c.encode_chunk(&"Chu".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Q2h1".into()))
);
assert_eq!(
c.finish_chunk()
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("".into()))
);
}
#[test]
fn eight_bit_encode() {
let mut c = EightBitCodec::new();
assert_eq!(
c.encode_chunk(&"Hello, world!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, world!".into()))
);
assert_eq!(
c.encode_chunk(&"Hello, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, мир!".into()))
);
}
#[test]
fn binary_encode() {
let mut c = BinaryCodec::new();
assert_eq!(
c.encode_chunk(&"Hello, world!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, world!".into()))
);
assert_eq!(
c.encode_chunk(&"Hello, мир!".into_buf())
.map(|s| from_utf8(&s).map(|s| String::from(s))),
Ok(Ok("Hello, мир!".into()))
);
}
}

View File

@@ -0,0 +1,121 @@
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},
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ContentTransferEncoding {
SevenBit,
QuotedPrintable,
Base64,
// 8BITMIME
EightBit,
Binary,
}
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::SevenBit
}
}
impl Display for ContentTransferEncoding {
fn fmt(&self, f: &mut FmtFormatter) -> FmtResult {
use self::ContentTransferEncoding::*;
f.write_str(match *self {
SevenBit => "7bit",
QuotedPrintable => "quoted-printable",
Base64 => "base64",
EightBit => "8bit",
Binary => "binary",
})
}
}
impl FromStr for ContentTransferEncoding {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::ContentTransferEncoding::*;
match s {
"7bit" => Ok(SevenBit),
"quoted-printable" => Ok(QuotedPrintable),
"base64" => Ok(Base64),
"8bit" => Ok(EightBit),
"binary" => Ok(Binary),
_ => Err(s.into()),
}
}
}
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))
}
}
#[cfg(test)]
mod test {
use super::ContentTransferEncoding;
use hyperx::header::Headers;
#[test]
fn format_content_transfer_encoding() {
let mut headers = Headers::new();
headers.set(ContentTransferEncoding::SevenBit);
assert_eq!(
format!("{}", headers),
"Content-Transfer-Encoding: 7bit\r\n"
);
headers.set(ContentTransferEncoding::Base64);
assert_eq!(
format!("{}", headers),
"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("Content-Transfer-Encoding", "base64");
assert_eq!(
headers.get::<ContentTransferEncoding>(),
Some(&ContentTransferEncoding::Base64)
);
}
}

View File

@@ -0,0 +1,289 @@
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};
/// Header which can contains multiple mailboxes
pub trait MailboxesHeader {
fn join_mailboxes(&mut self, other: Self);
}
macro_rules! mailbox_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq)]
pub struct $type_name(pub Mailbox);
impl Header for $type_name {
fn header_name() -> &'static 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 fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
f.fmt_line(&self.0.recode_name(utf8_b::encode))
}
}
};
}
macro_rules! mailboxes_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq)]
pub struct $type_name(pub Mailboxes);
impl MailboxesHeader for $type_name {
fn join_mailboxes(&mut self, other: Self) {
self.0.extend(other.0);
}
}
impl Header for $type_name {
fn header_name() -> &'static 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 fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
format_mailboxes(self.0.iter(), f)
}
}
};
}
mailbox_header! {
/**
`Sender` header
This header contains [`Mailbox`](::Mailbox) associated with sender.
```no_test
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
```
*/
(Sender, "Sender")
}
mailboxes_header! {
/**
`From` header
This header contains [`Mailboxes`](::Mailboxes).
*/
(From, "From")
}
mailboxes_header! {
/**
`Reply-To` header
This header contains [`Mailboxes`](::Mailboxes).
*/
(ReplyTo, "Reply-To")
}
mailboxes_header! {
/**
`To` header
This header contains [`Mailboxes`](::Mailboxes).
*/
(To, "To")
}
mailboxes_header! {
/**
`Cc` header
This header contains [`Mailboxes`](::Mailboxes).
*/
(Cc, "Cc")
}
mailboxes_header! {
/**
`Bcc` header
This header contains [`Mailboxes`](::Mailboxes).
*/
(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;
#[test]
fn format_single_without_name() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n");
}
#[test]
fn format_single_with_name() {
let from = Mailboxes::new().with("K. <kayo@example.com>".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(format!("{}", headers), "From: K. <kayo@example.com>\r\n");
}
#[test]
fn format_multi_without_name() {
let from = Mailboxes::new()
.with("kayo@example.com".parse().unwrap())
.with("pony@domain.tld".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(
format!("{}", headers),
"From: kayo@example.com, pony@domain.tld\r\n"
);
}
#[test]
fn format_multi_with_name() {
let from = vec![
"K. <kayo@example.com>".parse().unwrap(),
"Pony P. <pony@domain.tld>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.set(From(from.into()));
assert_eq!(
format!("{}", headers),
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n"
);
}
#[test]
fn format_single_with_utf8_name() {
let from = vec!["Кайо <kayo@example.com>".parse().unwrap()];
let mut headers = Headers::new();
headers.set(From(from.into()));
assert_eq!(
format!("{}", headers),
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
);
}
#[test]
fn parse_single_without_name() {
let from = vec!["kayo@example.com".parse().unwrap()].into();
let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com");
assert_eq!(headers.get::<From>(), Some(&From(from)));
}
#[test]
fn parse_single_with_name() {
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>");
assert_eq!(headers.get::<From>(), Some(&From(from)));
}
#[test]
fn parse_multi_without_name() {
let from: Vec<Mailbox> = vec![
"kayo@example.com".parse().unwrap(),
"pony@domain.tld".parse().unwrap(),
];
let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com, pony@domain.tld");
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
}
#[test]
fn parse_multi_with_name() {
let from: Vec<Mailbox> = vec![
"K. <kayo@example.com>".parse().unwrap(),
"Pony P. <pony@domain.tld>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
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())));
}
}

17
src/message/header/mod.rs Normal file
View File

@@ -0,0 +1,17 @@
/*!
## Headers widely used in email messages
*/
mod content;
mod mailbox;
mod special;
mod textual;
pub use self::{content::*, mailbox::*, special::*, textual::*};
pub use hyperx::header::{
Charset, ContentDisposition, ContentLocation, ContentType, Date, DispositionParam,
DispositionType, Header, Headers, HttpDate as EmailDate,
};

View File

@@ -0,0 +1,86 @@
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{fmt::Result as FmtResult, str::from_utf8};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MimeVersion {
pub major: u8,
pub minor: u8,
}
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 };
impl MimeVersion {
pub fn new(major: u8, minor: u8) -> Self {
MimeVersion { major, 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 s: Vec<&str> = from_utf8(r)
.map_err(|_| HeaderError::Header)?
.split('.')
.collect();
if s.len() != 2 {
return Err(HeaderError::Header);
}
let major = s[0].parse().map_err(|_| HeaderError::Header)?;
let minor = s[1].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;
#[test]
fn format_mime_version() {
let mut headers = Headers::new();
headers.set(MIME_VERSION_1_0);
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n");
headers.set(MimeVersion::new(0, 1));
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n");
}
#[test]
fn parse_mime_version() {
let mut headers = Headers::new();
headers.set_raw("MIME-Version", "1.0");
assert_eq!(headers.get::<MimeVersion>(), Some(&MIME_VERSION_1_0));
headers.set_raw("MIME-Version", "0.1");
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
}
}

View File

@@ -0,0 +1,105 @@
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};
macro_rules! text_header {
( $type_name: ident, $header_name: expr ) => {
#[derive(Debug, Clone, PartialEq)]
pub struct $type_name(pub String);
impl Header for $type_name {
fn header_name() -> &'static 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 fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
fmt_text(&self.0, f)
}
}
};
}
text_header!(Subject, "Subject");
text_header!(Comments, "Comments");
text_header!(Keywords, "Keywords");
text_header!(InReplyTo, "In-Reply-To");
text_header!(References, "References");
text_header!(MessageId, "Message-Id");
text_header!(UserAgent, "User-Agent");
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;
#[test]
fn format_ascii() {
let mut headers = Headers::new();
headers.set(Subject("Sample subject".into()));
assert_eq!(format!("{}", headers), "Subject: Sample subject\r\n");
}
#[test]
fn format_utf8() {
let mut headers = Headers::new();
headers.set(Subject("Тема сообщения".into()));
assert_eq!(
format!("{}", headers),
"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?=",
);
assert_eq!(
headers.get::<Subject>(),
Some(&Subject("Тема сообщения".into()))
);
}
}

View File

@@ -0,0 +1,5 @@
#[cfg(feature = "serde")]
mod serde;
mod types;
pub use self::types::*;

View File

@@ -0,0 +1,215 @@
use crate::message::{Mailbox, Mailboxes};
use serde::{
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use std::fmt::{Formatter, Result as FmtResult};
impl Serialize for Mailbox {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Mailbox {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
Name,
Email,
};
const FIELDS: &[&str] = &["name", "email"];
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
formatter.write_str("'name' or 'email'")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: DeError,
{
match value {
"name" => Ok(Field::Name),
"email" => Ok(Field::Email),
_ => Err(DeError::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct MailboxVisitor;
impl<'de> Visitor<'de> for MailboxVisitor {
type Value = Mailbox;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
formatter.write_str("mailbox string or object")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut name = None;
let mut addr = None;
while let Some(key) = map.next_key()? {
match key {
Field::Name => {
if name.is_some() {
return Err(DeError::duplicate_field("name"));
}
name = Some(map.next_value()?);
}
Field::Email => {
if addr.is_some() {
return Err(DeError::duplicate_field("email"));
}
addr = Some(map.next_value()?);
}
}
}
let addr = addr.ok_or_else(|| DeError::missing_field("email"))?;
Ok(Mailbox::new(name, addr))
}
}
deserializer.deserialize_any(MailboxVisitor)
}
}
impl Serialize for Mailboxes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Mailboxes {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct MailboxesVisitor;
impl<'de> Visitor<'de> for MailboxesVisitor {
type Value = Mailboxes;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
formatter.write_str("mailboxes string or sequence")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error>
where
V: SeqAccess<'de>,
{
let mut mboxes = Mailboxes::new();
while let Some(mbox) = seq.next_element()? {
mboxes.push(mbox);
}
Ok(mboxes)
}
}
deserializer.deserialize_any(MailboxesVisitor)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::address::Address;
use serde_json::from_str;
#[test]
fn parse_address_string() {
let m: Address = from_str(r#""kayo@example.com""#).unwrap();
assert_eq!(m, "kayo@example.com".parse().unwrap());
}
#[test]
fn parse_address_object() {
let m: Address = from_str(r#"{ "user": "kayo", "domain": "example.com" }"#).unwrap();
assert_eq!(m, "kayo@example.com".parse().unwrap());
}
#[test]
fn parse_mailbox_string() {
let m: Mailbox = from_str(r#""Kai <kayo@example.com>""#).unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
}
#[test]
fn parse_mailbox_object_address_stirng() {
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
}
#[test]
fn parse_mailbox_object_address_object() {
let m: Mailbox =
from_str(r#"{ "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }"#)
.unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
}
#[test]
fn parse_mailboxes_string() {
let m: Mailboxes =
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
assert_eq!(
m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse()
.unwrap()
);
}
#[test]
fn parse_mailboxes_array() {
let m: Mailboxes =
from_str(r#"["yin@dtb.com", { "name": "Hei", "email": "hei@dtb.com" }, { "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }]"#)
.unwrap();
assert_eq!(
m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse()
.unwrap()
);
}
}

View File

@@ -0,0 +1,325 @@
use crate::{
address::{Address, AddressError},
message::utf8_b,
};
use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult, Write},
slice::Iter,
str::FromStr,
};
/// Email address with optional addressee name
///
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailbox {
/// User name part
pub name: Option<String>,
/// Email address part
pub email: Address,
}
impl Mailbox {
/// Create new mailbox using email address and addressee name
#[inline]
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 {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
if let Some(ref name) = self.name {
let name = name.trim();
if !name.is_empty() {
f.write_str(&name)?;
f.write_str(" <")?;
self.email.fmt(f)?;
return f.write_char('>');
}
}
self.email.fmt(f)
}
}
impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
type Error = AddressError;
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
let (name, address) = header;
Ok(Mailbox::new(Some(name.into()), address.into().parse()?))
}
}
/*
impl<S: AsRef<&str>, T: AsRef<&str>> TryFrom<(S, T)> for Mailbox {
type Error = AddressError;
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
let (name, address) = header;
Ok(Mailbox::new(Some(name.as_ref()), address.as_ref().parse()?))
}
}*/
impl FromStr for Mailbox {
type Err = AddressError;
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
match (src.find('<'), src.find('>')) {
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => {
let name = src.split_at(addr_open).0;
let addr_open = addr_open + 1;
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
let addr = addr.parse()?;
let name = name.trim();
let name = if name.is_empty() {
None
} else {
Some(name.into())
};
Ok(Mailbox::new(name, addr))
}
(Some(_), _) => Err(AddressError::Unbalanced),
_ => {
let addr = src.parse()?;
Ok(Mailbox::new(None, addr))
}
}
}
}
/// List or email mailboxes
///
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailboxes(Vec<Mailbox>);
impl Mailboxes {
/// Create mailboxes list
#[inline]
pub fn new() -> Self {
Mailboxes(Vec::new())
}
/// Add mailbox to a list
#[inline]
pub fn with(mut self, mbox: Mailbox) -> Self {
self.0.push(mbox);
self
}
/// Add mailbox to a list
#[inline]
pub fn push(&mut self, mbox: Mailbox) {
self.0.push(mbox);
}
/// Extract first mailbox
#[inline]
pub fn into_single(self) -> Option<Mailbox> {
self.into()
}
/// Iterate over mailboxes
#[inline]
pub fn iter(&self) -> Iter<Mailbox> {
self.0.iter()
}
}
impl Default for Mailboxes {
fn default() -> Self {
Self::new()
}
}
impl From<Mailbox> for Mailboxes {
fn from(single: Mailbox) -> Self {
Mailboxes(vec![single])
}
}
impl Into<Option<Mailbox>> for Mailboxes {
fn into(self) -> Option<Mailbox> {
self.into_iter().next()
}
}
impl From<Vec<Mailbox>> for Mailboxes {
fn from(list: Vec<Mailbox>) -> Self {
Mailboxes(list)
}
}
impl Into<Vec<Mailbox>> for Mailboxes {
fn into(self) -> Vec<Mailbox> {
self.0
}
}
impl IntoIterator for Mailboxes {
type Item = Mailbox;
type IntoIter = ::std::vec::IntoIter<Mailbox>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
for elem in iter {
self.0.push(elem);
}
}
}
impl Display for Mailboxes {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let mut iter = self.iter();
if let Some(mbox) = iter.next() {
mbox.fmt(f)?;
for mbox in iter {
f.write_str(", ")?;
mbox.fmt(f)?;
}
}
Ok(())
}
}
impl FromStr for Mailboxes {
type Err = AddressError;
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))
}
})
})
.collect::<Result<Vec<_>, _>>()
.map(Mailboxes)
}
}
#[cfg(test)]
mod test {
use super::Mailbox;
use std::convert::TryInto;
#[test]
fn mailbox_format_address_only() {
assert_eq!(
format!(
"{}",
Mailbox::new(None, "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);
}
#[test]
fn mailbox_format_address_with_name() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
),
"K. <kayo@example.com>"
);
}
#[test]
fn format_address_with_empty_name() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("".into()), "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);
}
#[test]
fn format_address_with_name_trim() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
),
"K. <kayo@example.com>"
);
}
#[test]
fn parse_address_only() {
assert_eq!(
"kayo@example.com".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_name() {
assert_eq!(
"K. <kayo@example.com>".parse(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
#[test]
fn parse_address_with_empty_name() {
assert_eq!(
"<kayo@example.com>".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_empty_name_trim() {
assert_eq!(
" <kayo@example.com>".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_from_tuple() {
assert_eq!(
("K.".to_string(), "kayo@example.com".to_string()).try_into(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
}

636
src/message/mimebody.rs Normal file
View File

@@ -0,0 +1,636 @@
use crate::message::{
encoder::codec,
header::{ContentTransferEncoding, ContentType, Header, Headers},
};
use bytes::{Bytes, IntoBuf};
use mime::Mime;
use std::{
fmt::{Display, Error as FmtError, Formatter, Result as FmtResult},
str::from_utf8,
};
use textnonce::TextNonce;
/// MIME part variants
///
#[derive(Debug, Clone)]
pub enum Part<B = Bytes> {
/// Single part with content
///
Single(SinglePart<B>),
/// Multiple parts of content
///
Multi(MultiPart<B>),
}
impl<B> Display for Part<B>
where
B: AsRef<str>,
{
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match *self {
Part::Single(ref part) => part.fmt(f),
Part::Multi(ref part) => part.fmt(f),
}
}
}
/// Parts of multipart body
///
pub type Parts<B = Bytes> = Vec<Part<B>>;
/// Creates builder for single part
///
#[derive(Debug, Clone)]
pub struct SinglePartBuilder {
headers: Headers,
}
impl SinglePartBuilder {
/// Creates a default singlepart builder
pub fn new() -> Self {
Self {
headers: Headers::new(),
}
}
/// Set the header to singlepart
#[inline]
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Build singlepart using body
#[inline]
pub fn body<T>(self, body: T) -> SinglePart<T> {
SinglePart {
headers: self.headers,
body,
}
}
}
impl Default for SinglePartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Single part
///
/// # Example
///
/// ```no_test
/// extern crate mime;
/// extern crate emailmessage;
///
/// use emailmessage::{SinglePart, header};
///
/// let part = SinglePart::builder()
/// .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
/// .header(header::ContentTransferEncoding::Binary)
/// .body("Текст письма в уникоде");
/// ```
///
#[derive(Debug, Clone)]
pub struct SinglePart<B = Bytes> {
headers: Headers,
body: B,
}
impl SinglePart<()> {
/// Creates a default builder for singlepart
pub fn builder() -> SinglePartBuilder {
SinglePartBuilder::new()
}
/// Creates a singlepart builder with 7bit encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::SevenBit)`.
pub fn seven_bit() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::SevenBit)
}
/// Creates a singlepart builder with quoted-printable encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::QuotedPrintable)`.
pub fn quoted_printable() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::QuotedPrintable)
}
/// Creates a singlepart builder with base64 encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Base64)`.
pub fn base64() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::Base64)
}
/// Creates a singlepart builder with 8-bit encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::EightBit)`.
#[inline]
pub fn eight_bit() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::EightBit)
}
/// Creates a singlepart builder with binary encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Binary)`.
#[inline]
pub fn binary() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::Binary)
}
}
impl<B> SinglePart<B> {
/// Get the transfer encoding
#[inline]
pub fn encoding(&self) -> Option<&ContentTransferEncoding> {
self.headers.get()
}
/// Get the headers from singlepart
#[inline]
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Get a mutable reference to the headers
#[inline]
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}
/// Read the body from singlepart
#[inline]
pub fn body_ref(&self) -> &B {
&self.body
}
}
impl<B> Display for SinglePart<B>
where
B: AsRef<str>,
{
fn fmt(&self, f: &mut Formatter) -> FmtResult {
self.headers.fmt(f)?;
"\r\n".fmt(f)?;
let body = self.body.as_ref();
let mut encoder = codec(self.encoding());
let result = encoder
.encode_all(&body.into_buf())
.map_err(|_| FmtError::default())?;
let body = from_utf8(&result).map_err(|_| FmtError::default())?;
body.fmt(f)?;
"\r\n".fmt(f)
}
}
/// The kind of multipart
///
#[derive(Debug, Clone, Copy)]
pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts
///
/// For example this kind can be used to mix email message and attachments.
Mixed,
/// Alternative kind to join several variants of same email contents.
///
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message.
Alternative,
/// Related kind to mix content and related resources.
///
/// For example, you can include images into HTML content using that.
Related,
}
impl MultiPartKind {
fn to_mime<S: AsRef<str>>(self, boundary: Option<S>) -> Mime {
let boundary = boundary
.map(|s| s.as_ref().into())
.unwrap_or_else(|| TextNonce::sized(68).unwrap().into_string());
use self::MultiPartKind::*;
format!(
"multipart/{}; boundary=\"{}\"",
match self {
Mixed => "mixed",
Alternative => "alternative",
Related => "related",
},
boundary
)
.parse()
.unwrap()
}
fn from_mime(m: &Mime) -> Option<Self> {
use self::MultiPartKind::*;
match m.subtype().as_ref() {
"mixed" => Some(Mixed),
"alternative" => Some(Alternative),
"related" => Some(Related),
_ => None,
}
}
}
impl From<MultiPartKind> for Mime {
fn from(m: MultiPartKind) -> Self {
m.to_mime::<String>(None)
}
}
/// Multipart builder
///
#[derive(Debug, Clone)]
pub struct MultiPartBuilder {
headers: Headers,
}
impl MultiPartBuilder {
/// Creates default multipart builder
#[inline]
pub fn new() -> Self {
Self {
headers: Headers::new(),
}
}
/// Set a header
#[inline]
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Set `Content-Type` header using [`MultiPartKind`]
#[inline]
pub fn kind(self, kind: MultiPartKind) -> Self {
self.header(ContentType(kind.into()))
}
/// Set custom boundary
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
let kind = {
let mime = &self.headers.get::<ContentType>().unwrap().0;
MultiPartKind::from_mime(mime).unwrap()
};
let mime = kind.to_mime(Some(boundary.as_ref()));
self.header(ContentType(mime))
}
/// Creates multipart without parts
#[inline]
pub fn build<B>(self) -> MultiPart<B> {
MultiPart {
headers: self.headers,
parts: Vec::new(),
}
}
/// Creates multipart using part
#[inline]
pub fn part<B>(self, part: Part<B>) -> MultiPart<B> {
self.build().part(part)
}
/// Creates multipart using singlepart
#[inline]
pub fn singlepart<B>(self, part: SinglePart<B>) -> MultiPart<B> {
self.build().singlepart(part)
}
/// Creates multipart using multipart
#[inline]
pub fn multipart<B>(self, part: MultiPart<B>) -> MultiPart<B> {
self.build().multipart(part)
}
}
impl Default for MultiPartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Multipart variant with parts
///
#[derive(Debug, Clone)]
pub struct MultiPart<B = Bytes> {
headers: Headers,
parts: Parts<B>,
}
impl MultiPart<()> {
/// Creates multipart builder
#[inline]
pub fn builder() -> MultiPartBuilder {
MultiPartBuilder::new()
}
/// Creates mixed multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)`
#[inline]
pub fn mixed() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Mixed)
}
/// Creates alternative multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)`
#[inline]
pub fn alternative() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Alternative)
}
/// Creates related multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)`
#[inline]
pub fn related() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Related)
}
}
impl<B> MultiPart<B> {
/// Add part to multipart
#[inline]
pub fn part(mut self, part: Part<B>) -> Self {
self.parts.push(part);
self
}
/// Add single part to multipart
#[inline]
pub fn singlepart(mut self, part: SinglePart<B>) -> Self {
self.parts.push(Part::Single(part));
self
}
/// Add multi part to multipart
#[inline]
pub fn multipart(mut self, part: MultiPart<B>) -> Self {
self.parts.push(Part::Multi(part));
self
}
/// Get the boundary of multipart contents
#[inline]
pub fn boundary(&self) -> String {
let content_type = &self.headers.get::<ContentType>().unwrap().0;
content_type.get_param("boundary").unwrap().as_str().into()
}
/// Get the headers from the multipart
#[inline]
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Get a mutable reference to the headers
#[inline]
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}
/// Get the parts from the multipart
#[inline]
pub fn parts(&self) -> &Parts<B> {
&self.parts
}
/// Get a mutable reference to the parts
#[inline]
pub fn parts_mut(&mut self) -> &mut Parts<B> {
&mut self.parts
}
}
impl<B> Display for MultiPart<B>
where
B: AsRef<str>,
{
fn fmt(&self, f: &mut Formatter) -> FmtResult {
self.headers.fmt(f)?;
"\r\n".fmt(f)?;
let boundary = self.boundary();
for part in &self.parts {
"--".fmt(f)?;
boundary.fmt(f)?;
"\r\n".fmt(f)?;
part.fmt(f)?;
}
"--".fmt(f)?;
boundary.fmt(f)?;
"--\r\n".fmt(f)
}
}
#[cfg(test)]
mod test {
use super::{MultiPart, Part, SinglePart};
use crate::message::header;
#[test]
fn single_part_binary() {
let part: SinglePart<String> = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
format!("{}", part),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n"
)
);
}
#[test]
fn single_part_quoted_printable() {
let part: SinglePart<String> = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::QuotedPrintable)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
format!("{}", part),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
"\r\n",
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5\r\n"
)
);
}
#[test]
fn single_part_base64() {
let part: SinglePart<String> = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Base64)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
format!("{}", part),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
)
);
}
#[test]
fn multi_part_mixed() {
let part: MultiPart<String> = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")),
))
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"example.c".as_bytes().into(),
)],
})
.header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")),
);
assert_eq!(format!("{}", part),
concat!("Content-Type: multipart/mixed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-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"));
}
#[test]
fn multi_part_alternative() {
let part: MultiPart<String> = MultiPart::alternative()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"))))
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
assert_eq!(format!("{}", part),
concat!("Content-Type: multipart/alternative;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/html; charset=utf8\r\n",
"Content-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"));
}
#[test]
fn multi_part_mixed_related() {
let part: MultiPart<String> = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.multipart(MultiPart::related()
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
.singlepart(SinglePart::builder()
.header(header::ContentType("image/png".parse().unwrap()))
.header(header::ContentLocation("/image.png".into()))
.header(header::ContentTransferEncoding::Base64)
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
.singlepart(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".as_bytes().into())]
})
.header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")));
assert_eq!(format!("{}", part),
concat!("Content-Type: multipart/mixed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: multipart/related;",
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
"\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
"Content-Type: text/html; charset=utf8\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",
"Content-Type: image/png\r\n",
"Content-Location: /image.png\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\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"));
}
}

329
src/message/mod.rs Normal file
View File

@@ -0,0 +1,329 @@
//! Provides a strongly typed way to build emails
pub use encoder::*;
pub use mailbox::*;
pub use mimebody::*;
pub use mime;
mod encoder;
pub mod header;
mod mailbox;
mod mimebody;
mod utf8_b;
use crate::{
message::header::{EmailDate, Header, Headers, MailboxesHeader},
Envelope, Error as EmailError,
};
use bytes::Bytes;
use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult},
time::SystemTime,
};
use uuid::Uuid;
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
/// A builder for messages
#[derive(Debug, Clone)]
pub struct MessageBuilder {
headers: Headers,
}
impl MessageBuilder {
/// Creates a new default message builder
#[inline]
pub fn new() -> Self {
Self {
headers: Headers::new(),
}
}
/// Set custom header to message
#[inline]
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// 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)
}
}
/// Add `Date` header to message
///
/// Shortcut for `self.header(header::Date(date))`.
#[inline]
pub fn date(self, date: EmailDate) -> Self {
self.header(header::Date(date))
}
/// Set `Date` header using current date/time
///
/// Shortcut for `self.date(SystemTime::now())`.
#[inline]
pub fn date_now(self) -> Self {
self.date(SystemTime::now().into())
}
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
#[inline]
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
self.header(header::Subject(subject.into()))
}
/// Set `Mime-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
#[inline]
pub fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0)
}
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
///
/// Shortcut for `self.header(header::Sender(mbox))`.
#[inline]
pub fn sender(self, mbox: Mailbox) -> Self {
self.header(header::Sender(mbox))
}
/// Set or add mailbox to `From` header
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
///
/// Shortcut for `self.mailbox(header::From(mbox))`.
#[inline]
pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From(mbox.into()))
}
/// Set or add mailbox to `ReplyTo` header
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
///
/// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
#[inline]
pub fn reply_to(self, mbox: Mailbox) -> Self {
self.mailbox(header::ReplyTo(mbox.into()))
}
/// Set or add mailbox to `To` header
///
/// Shortcut for `self.mailbox(header::To(mbox))`.
#[inline]
pub fn to(self, mbox: Mailbox) -> Self {
self.mailbox(header::To(mbox.into()))
}
/// Set or add mailbox to `Cc` header
///
/// Shortcut for `self.mailbox(header::Cc(mbox))`.
#[inline]
pub fn cc(self, mbox: Mailbox) -> Self {
self.mailbox(header::Cc(mbox.into()))
}
/// Set or add mailbox to `Bcc` header
///
/// Shortcut for `self.mailbox(header::Bcc(mbox))`.
#[inline]
pub fn bcc(self, mbox: Mailbox) -> Self {
self.mailbox(header::Bcc(mbox.into()))
}
/// Set or add message id to [`In-Reply-To`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
#[inline]
pub fn in_reply_to(self, id: String) -> Self {
self.header(header::InReplyTo(id))
}
/// Set or add message id to [`References`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
#[inline]
pub fn references(self, id: String) -> Self {
self.header(header::References(id))
}
/// Set [Message-Id
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
///
/// Should generally be inserted by the mail relay.
///
/// If `None` is provided, an id will be generated in the
/// `<UUID@HOSTNAME>`.
#[inline]
pub fn message_id(self, id: Option<String>) -> Self {
match id {
Some(i) => self.header(header::MessageId(i)),
None => {
#[cfg(feature = "hostname")]
let hostname = hostname::get()
.map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string());
#[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
self.header(header::MessageId(
// https://tools.ietf.org/html/rfc5322#section-3.6.4
format!("<{}@{}>", Uuid::new_v4(), hostname),
))
}
}
}
/// Set [User-Agent
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
#[inline]
pub fn user_agent(self, id: String) -> Self {
self.header(header::UserAgent(id))
}
fn insert_missing_headers(self) -> Self {
// Insert Date if missing
if self.headers.get::<header::Date>().is_none() {
self.date_now()
} else {
self
}
// TODO insert sender if needed?
}
// TODO: High-level methods for attachments and embedded files
/// Create message by joining content
#[inline]
fn build<T>(self, body: T, split: bool) -> Result<Message<T>, EmailError> {
let res = self.insert_missing_headers();
let envelope = Envelope::try_from(&res.headers)?;
Ok(Message {
headers: res.headers,
split,
body,
envelope,
})
}
/// Create message using body
#[inline]
pub fn body<T>(self, body: T) -> Result<Message<T>, EmailError> {
self.build(body, true)
}
/// Create message using mime body ([`MultiPart`](::MultiPart) or [`SinglePart`](::SinglePart))
// FIXME restrict usage on MIME?
#[inline]
pub fn mime_body<T>(self, body: T) -> Result<Message<T>, EmailError> {
self.mime_1_0().build(body, false)
}
}
/// Email message which can be formatted
#[derive(Clone, Debug)]
pub struct Message<B = Bytes> {
headers: Headers,
split: bool,
body: B,
envelope: Envelope,
}
impl Message<()> {
/// Create a new message builder without headers
#[inline]
pub fn builder() -> MessageBuilder {
MessageBuilder::new()
}
}
impl<B> Message<B> {
/// Get the headers from the Message
#[inline]
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Read the body
#[inline]
pub fn body_ref(&self) -> &B {
&self.body
}
/// Try to extract envelope data from `Message` headers
#[inline]
pub fn envelope(&self) -> &Envelope {
&self.envelope
}
}
impl Default for MessageBuilder {
fn default() -> Self {
MessageBuilder::new()
}
}
impl<B> Display for Message<B>
where
B: Display,
{
fn fmt(&self, f: &mut Formatter) -> FmtResult {
self.headers.fmt(f)?;
if self.split {
f.write_str("\r\n")?;
}
self.body.fmt(f)
}
}
// An email is Message + Envelope
#[cfg(test)]
mod test {
use crate::message::{header, mailbox::Mailbox, Message};
#[test]
fn email_message() {
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
let email = Message::builder()
.date(date)
.header(header::From(
vec![Mailbox::new(
Some("Каи".into()),
"kayo@example.com".parse().unwrap(),
)]
.into(),
))
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject("яңа ел белән!".into()))
.body("Happy new year!")
.unwrap();
assert_eq!(
format!("{}", email),
concat!(
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: Pony O.P. <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"\r\n",
"Happy new year!"
)
);
}
}

67
src/message/utf8_b.rs Normal file
View File

@@ -0,0 +1,67 @@
use std::str::from_utf8;
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> {
let s = s.trim();
if s.starts_with("=?utf-8?b?") && s.ends_with("?=") {
let s = s.split_at(10).1;
let s = s.split_at(s.len() - 2).0;
base64::decode(s)
.map_err(|_| ())
.and_then(|v| {
if let Ok(s) = from_utf8(&v) {
Ok(Some(s.into()))
} else {
Err(())
}
})
.unwrap_or(None)
} else {
Some(s.into())
}
}
#[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

@@ -1,11 +1,10 @@
//! Error and result type for file transport
use self::Error::*;
use serde_json;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
};
/// An enum of all error kinds.
@@ -21,20 +20,16 @@ pub enum Error {
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
match *self {
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
JsonSerialization(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Client(err) => err,
Io(ref err) => err.description(),
JsonSerialization(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&dyn StdError> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err),
@@ -61,5 +56,7 @@ impl From<&'static str> for Error {
}
}
type Id = String;
/// SMTP result type
pub type FileResult = Result<(), Error>;
pub type FileResult = Result<Id, Error>;

View File

@@ -3,14 +3,14 @@
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
//!
use crate::file::error::FileResult;
use crate::Email;
use crate::Envelope;
use crate::Transport;
use serde_json;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use crate::{transport::file::error::FileResult, Envelope, Message, Transport};
use std::{
fmt::Display,
fs::File,
io::prelude::*,
path::{Path, PathBuf},
};
use uuid::Uuid;
pub mod error;
@@ -34,29 +34,27 @@ impl FileTransport {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct SerializableEmail {
envelope: Envelope,
message_id: String,
message: Vec<u8>,
}
impl<'a> Transport<'a> for FileTransport {
impl<'a, B> Transport<'a, B> for FileTransport {
type Result = FileResult;
fn send<E: Into<Email>>(&mut self, email: E) -> FileResult {
let email = email.into();
let message_id = email.message_id().to_string();
let envelope = email.envelope().clone();
fn send(&mut self, email: Message<B>) -> Self::Result
where
B: Display,
{
let email_id = Uuid::new_v4();
let mut file = self.path.clone();
file.push(format!("{}.json", message_id));
file.push(format!("{}.json", email_id));
let serialized = serde_json::to_string(&SerializableEmail {
envelope,
message_id,
message: email.message_to_string()?.as_bytes().to_vec(),
envelope: email.envelope().clone(),
message: email.to_string().into_bytes(),
})?;
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
Ok(())
Ok(email_id.to_string())
}
}

7
src/transport/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
#[cfg(feature = "file-transport")]
pub mod file;
#[cfg(feature = "sendmail-transport")]
pub mod sendmail;
#[cfg(feature = "smtp-transport")]
pub mod smtp;
pub mod stub;

View File

@@ -1,11 +1,11 @@
//! Error and result type for sendmail transport
use self::Error::*;
use std::io;
use std::string::FromUtf8Error;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
string::FromUtf8Error,
};
/// An enum of all error kinds.
@@ -13,7 +13,7 @@ use std::{
pub enum Error {
/// Internal client error
Client(String),
/// Error parsing UTF8in response
/// Error parsing UTF8 in response
Utf8Parsing(FromUtf8Error),
/// IO error
Io(io::Error),
@@ -21,20 +21,16 @@ pub enum Error {
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
match *self {
Client(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Io(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Client(ref err) => err,
Utf8Parsing(ref err) => err.description(),
Io(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&dyn StdError> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Io(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),

View File

@@ -1,14 +1,15 @@
//! The sendmail transport sends the email using the local sendmail command.
//!
use crate::sendmail::error::SendmailResult;
use crate::Email;
use crate::Transport;
use crate::{transport::sendmail::error::SendmailResult, Message, Transport};
use log::info;
use std::convert::AsRef;
use std::io::prelude::*;
use std::io::Read;
use std::process::{Command, Stdio};
use std::{
convert::AsRef,
fmt::Display,
io::prelude::*,
process::{Command, Stdio},
};
use uuid::Uuid;
pub mod error;
@@ -35,34 +36,38 @@ impl SendmailTransport {
}
}
impl<'a> Transport<'a> for SendmailTransport {
impl<'a, B> Transport<'a, B> for SendmailTransport {
type Result = SendmailResult;
fn send<E: Into<Email>>(&mut self, email: E) -> SendmailResult {
let email = email.into();
let message_id = email.message_id().to_string();
fn send(&mut self, email: Message<B>) -> Self::Result
where
B: Display,
{
let email_id = Uuid::new_v4();
// Spawn the sendmail command
let mut process = Command::new(&self.command)
.arg("-i")
.arg("-f")
.arg(email.envelope().from().map(AsRef::as_ref).unwrap_or("\"\""))
.args(email.envelope.to())
.arg(
email
.envelope()
.from()
.map(|f| f.as_ref())
.unwrap_or("\"\""),
)
.args(email.envelope().to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content);
process
.stdin
.as_mut()
.unwrap()
.write_all(message_content.as_bytes())?;
.write_all(email.to_string().as_bytes())?;
info!("Wrote {} message to stdin", message_id);
info!("Wrote {} message to stdin", email_id);
let output = process.wait_with_output()?;

View File

@@ -1,6 +1,6 @@
//! Provides limited SASL authentication mechanisms
use crate::smtp::error::Error;
use crate::transport::smtp::error::Error;
use std::fmt::{self, Display, Formatter};
/// Accepted authentication mechanisms on an encrypted connection

View File

@@ -1,8 +1,10 @@
#![allow(missing_docs)]
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
use std::io::{self, Cursor, Read, Write};
use std::sync::{Arc, Mutex};
use std::{
io::{self, Cursor, Read, Write},
sync::{Arc, Mutex},
};
pub type MockCursor = Cursor<Vec<u8>>;

View File

@@ -1,20 +1,23 @@
//! SMTP client
use crate::smtp::authentication::{Credentials, Mechanism};
use crate::smtp::client::net::ClientTlsParameters;
use crate::smtp::client::net::{Connector, NetworkStream, Timeout};
use crate::smtp::commands::*;
use crate::smtp::error::{Error, SmtpResult};
use crate::smtp::response::Response;
use crate::transport::smtp::{
authentication::{Credentials, Mechanism},
client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout},
commands::*,
error::{Error, SmtpResult},
response::Response,
};
use bufstream::BufStream;
use log::debug;
#[cfg(feature = "serde")]
use std::fmt::Debug;
use std::fmt::Display;
use std::io::{self, BufRead, BufReader, Read, Write};
use std::net::ToSocketAddrs;
use std::string::String;
use std::time::Duration;
use std::{
fmt::Display,
io::{self, BufRead, Read, Write},
net::ToSocketAddrs,
string::String,
time::Duration,
};
pub mod mock;
pub mod net;
@@ -33,7 +36,6 @@ impl ClientCodec {
}
/// Adds transparency
/// TODO: replace CR and LF by CRLF
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) -> Result<(), Error> {
match frame.len() {
0 => {
@@ -174,7 +176,7 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
// TODO
// Limit challenges to avoid blocking
let mut challenges = 10;
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?;
@@ -195,31 +197,11 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
}
/// Sends the message content
pub fn message(&mut self, message: Box<dyn Read>) -> SmtpResult {
pub fn message(&mut self, message: &[u8]) -> SmtpResult {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new();
let mut message_reader = BufReader::new(message);
loop {
out_buf.clear();
let consumed = match message_reader.fill_buf() {
Ok(bytes) => {
codec.encode(bytes, &mut out_buf)?;
bytes.len()
}
Err(ref err) => panic!("Failed with: {}", err),
};
message_reader.consume(consumed);
if consumed == 0 {
break;
}
self.write(out_buf.as_slice())?;
}
codec.encode(message, &mut out_buf)?;
self.write(out_buf.as_slice())?;
self.write(b"\r\n.\r\n")?;
self.read_response()
}

View File

@@ -1,18 +1,19 @@
//! A trait to represent a stream
use crate::smtp::client::mock::MockStream;
use crate::smtp::error::Error;
use crate::transport::smtp::{client::mock::MockStream, error::Error};
#[cfg(feature = "native-tls")]
use native_tls::{TlsConnector, TlsStream};
#[cfg(feature = "rustls")]
use rustls::{ClientConfig, ClientSession};
#[cfg(feature = "native-tls")]
use std::io::ErrorKind;
use std::io::{self, Read, Write};
use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream};
#[cfg(feature = "rustls")]
use std::sync::Arc;
use std::time::Duration;
use std::{
io::{self, Read, Write},
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream},
time::Duration,
};
/// Parameters to use for secure clients
#[derive(Clone)]

View File

@@ -1,15 +1,19 @@
//! SMTP commands
use crate::smtp::authentication::{Credentials, Mechanism};
use crate::smtp::error::Error;
use crate::smtp::extension::ClientId;
use crate::smtp::extension::{MailParameter, RcptParameter};
use crate::smtp::response::Response;
use crate::EmailAddress;
use base64;
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
error::Error,
extension::{ClientId, MailParameter, RcptParameter},
response::Response,
},
Address,
};
use log::debug;
use std::convert::AsRef;
use std::fmt::{self, Display, Formatter};
use std::{
convert::AsRef,
fmt::{self, Display, Formatter},
};
/// EHLO command
#[derive(PartialEq, Clone, Debug)]
@@ -47,7 +51,7 @@ impl Display for StarttlsCommand {
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MailCommand {
sender: Option<EmailAddress>,
sender: Option<Address>,
parameters: Vec<MailParameter>,
}
@@ -56,7 +60,7 @@ impl Display for MailCommand {
write!(
f,
"MAIL FROM:<{}>",
self.sender.as_ref().map(AsRef::as_ref).unwrap_or("")
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("")
)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
@@ -67,7 +71,7 @@ impl Display for MailCommand {
impl MailCommand {
/// Creates a MAIL command
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> MailCommand {
MailCommand { sender, parameters }
}
}
@@ -76,7 +80,7 @@ impl MailCommand {
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RcptCommand {
recipient: EmailAddress,
recipient: Address,
parameters: Vec<RcptParameter>,
}
@@ -92,7 +96,7 @@ impl Display for RcptCommand {
impl RcptCommand {
/// Creates an RCPT command
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> RcptCommand {
RcptCommand {
recipient,
parameters,
@@ -247,7 +251,7 @@ impl AuthCommand {
challenge: Option<String>,
) -> Result<AuthCommand, Error> {
let response = if mechanism.supports_initial_response() || challenge.is_some() {
Some(mechanism.response(&credentials, challenge.as_ref().map(String::as_str))?)
Some(mechanism.response(&credentials, challenge.as_deref())?)
} else {
None
};
@@ -292,12 +296,13 @@ impl AuthCommand {
#[cfg(test)]
mod test {
use super::*;
use crate::smtp::extension::MailBodyParameter;
use crate::transport::smtp::extension::MailBodyParameter;
use std::str::FromStr;
#[test]
fn test_display() {
let id = ClientId::Domain("localhost".to_string());
let email = EmailAddress::new("test@example.com".to_string()).unwrap();
let email = Address::from_str("test@example.com").unwrap();
let mail_parameter = MailParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),

View File

@@ -1,16 +1,16 @@
//! Error and result type for SMTP clients
use self::Error::*;
use crate::smtp::response::{Response, Severity};
use crate::transport::smtp::response::{Response, Severity};
use base64::DecodeError;
#[cfg(feature = "native-tls")]
use native_tls;
use nom;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
use std::string::FromUtf8Error;
use std::{
error::Error as StdError,
fmt,
fmt::{Display, Formatter},
io,
string::FromUtf8Error,
};
/// An enum of all error kinds.
#[derive(Debug)]
@@ -46,40 +46,36 @@ pub enum Error {
}
impl Display for Error {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
match *self {
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
Transient(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "transient error during SMTP transaction",
}),
Permanent(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "permanent error during SMTP transaction",
}),
ResponseParsing(err) => fmt.write_str(err),
ChallengeParsing(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Resolution => fmt.write_str("could not resolve hostname"),
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
#[cfg(feature = "native-tls")]
Tls(ref err) => err.fmt(fmt),
Parsing(ref err) => fmt.write_str(err.description()),
#[cfg(feature = "rustls-tls")]
InvalidDNSName(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
fn description(&self) -> &str {
match *self {
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
Transient(ref err) => match err.first_line() {
Some(line) => line,
None => "undetailed transient error during SMTP transaction",
},
Permanent(ref err) => match err.first_line() {
Some(line) => line,
None => "undetailed permanent error during SMTP transaction",
},
ResponseParsing(err) => err,
ChallengeParsing(ref err) => err.description(),
Utf8Parsing(ref err) => err.description(),
Resolution => "could not resolve hostname",
Client(err) => err,
Io(ref err) => err.description(),
#[cfg(feature = "native-tls")]
Tls(ref err) => err.description(),
Parsing(ref err) => err.description(),
#[cfg(feature = "rustls-tls")]
InvalidDNSName(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&dyn StdError> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
ChallengeParsing(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),

View File

@@ -1,13 +1,14 @@
//! ESMTP features
use crate::smtp::authentication::Mechanism;
use crate::smtp::error::Error;
use crate::smtp::response::Response;
use crate::smtp::util::XText;
use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::result::Result;
use crate::transport::smtp::{
authentication::Mechanism, error::Error, response::Response, util::XText,
};
use std::{
collections::HashSet,
fmt::{self, Display, Formatter},
net::{Ipv4Addr, Ipv6Addr},
result::Result,
};
/// Default client id
const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
@@ -42,6 +43,7 @@ impl ClientId {
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
/// found
#[cfg(feature = "hostname")]
pub fn hostname() -> ClientId {
ClientId::Domain(
hostname::get()
@@ -264,8 +266,10 @@ impl Display for RcptParameter {
mod test {
use super::{ClientId, Extension, ServerInfo};
use crate::smtp::authentication::Mechanism;
use crate::smtp::response::{Category, Code, Detail, Response, Severity};
use crate::transport::smtp::{
authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity},
};
use std::collections::HashSet;
#[test]

View File

@@ -13,22 +13,29 @@
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
use crate::smtp::authentication::{
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
use crate::{
transport::smtp::{
authentication::{
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
},
client::{net::ClientTlsParameters, InnerClient},
commands::*,
error::{Error, SmtpResult},
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
},
Message, Transport,
};
use crate::smtp::client::net::ClientTlsParameters;
use crate::smtp::client::InnerClient;
use crate::smtp::commands::*;
use crate::smtp::error::{Error, SmtpResult};
use crate::smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
use crate::{Email, Transport};
use log::{debug, info};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls")]
use rustls::ClientConfig;
use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
use std::{
fmt::Display,
net::{SocketAddr, ToSocketAddrs},
time::Duration,
};
use uuid::Uuid;
pub mod authentication;
pub mod client;
@@ -130,7 +137,10 @@ impl SmtpClient {
smtp_utf8: false,
credentials: None,
connection_reuse: ConnectionReuseParameters::NoReuse,
#[cfg(feature = "hostname")]
hello_name: ClientId::hostname(),
#[cfg(not(feature = "hostname"))]
hello_name: ClientId::new("localhost".to_string()),
authentication_mechanism: None,
force_set_auth: false,
timeout: Some(Duration::new(60, 0)),
@@ -420,7 +430,7 @@ impl<'a> SmtpTransport {
}
}
impl<'a> Transport<'a> for SmtpTransport {
impl<'a, B> Transport<'a, B> for SmtpTransport {
type Result = SmtpResult;
/// Sends an email
@@ -428,10 +438,12 @@ impl<'a> Transport<'a> for SmtpTransport {
feature = "cargo-clippy",
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
)]
fn send<E: Into<Email>>(&mut self, email: E) -> SmtpResult {
let email = email.into();
let message_id = email.message_id().to_string();
fn send(&mut self, email: Message<B>) -> Self::Result
where
B: Display,
{
let email_id = Uuid::new_v4();
let envelope = email.envelope();
if !self.client.is_connected() {
self.connect()?;
@@ -460,39 +472,37 @@ impl<'a> Transport<'a> for SmtpTransport {
}
try_smtp!(
self.client.command(MailCommand::new(
email.envelope().from().cloned(),
mail_options,
)),
self.client
.command(MailCommand::new(envelope.from().cloned(), mail_options,)),
self
);
// Log the mail command
info!(
"{}: from=<{}>",
message_id,
match email.envelope().from() {
email_id,
match envelope.from() {
Some(address) => address.to_string(),
None => "".to_string(),
}
);
// Recipient
for to_address in email.envelope().to() {
for to_address in envelope.to() {
try_smtp!(
self.client
.command(RcptCommand::new(to_address.clone(), vec![])),
self
);
// Log the rcpt command
info!("{}: to=<{}>", message_id, to_address);
info!("{}: to=<{}>", email_id, to_address);
}
// Data
try_smtp!(self.client.command(DataCommand), self);
// Message content
let result = self.client.message(Box::new(email.message()));
let result = self.client.message(email.to_string().as_bytes());
if let Ok(ref result) = result {
// Increment the connection reuse counter
@@ -501,7 +511,7 @@ impl<'a> Transport<'a> for SmtpTransport {
// Log the message
info!(
"{}: conn_use={}, status=sent ({})",
message_id,
email_id,
self.state.connection_reuse_count,
result
.message

View File

@@ -1,5 +1,4 @@
use crate::smtp::error::Error;
use crate::smtp::{ConnectionReuseParameters, SmtpClient, SmtpTransport};
use crate::transport::smtp::{error::Error, ConnectionReuseParameters, SmtpClient, SmtpTransport};
use r2d2::ManageConnection;
pub struct SmtpConnectionManager {

View File

@@ -1,7 +1,7 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use crate::smtp::Error;
use crate::transport::smtp::Error;
use nom::{
branch::alt,
bytes::complete::{tag, take_until},
@@ -10,10 +10,12 @@ use nom::{
sequence::{preceded, tuple},
IResult,
};
use std::fmt::{Display, Formatter, Result};
use std::result;
use std::str::FromStr;
use std::string::ToString;
use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
string::ToString,
};
/// First digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)]

View File

@@ -2,9 +2,9 @@
//! testing purposes.
//!
use crate::Email;
use crate::Transport;
use crate::{Message, Transport};
use log::info;
use std::fmt::Display;
/// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)]
@@ -27,15 +27,18 @@ impl StubTransport {
/// SMTP result type
pub type StubResult = Result<(), ()>;
impl<'a> Transport<'a> for StubTransport {
impl<'a, B> Transport<'a, B> for StubTransport
where
B: Display,
{
type Result = StubResult;
fn send<E: Into<Email>>(&mut self, email: E) -> StubResult {
let email = email.into();
fn send(&mut self, email: Message<B>) -> Self::Result
where
B: Display,
{
info!(
"{}: from=<{}> to=<{:?}>",
email.message_id(),
"from=<{}> to=<{:?}>",
match email.envelope().from() {
Some(address) => address.to_string(),
None => "".to_string(),