Compare commits

...

15 Commits

Author SHA1 Message Date
Alexis Mousset
5e474677f9 Prepare 0.9.6 (#630) 2021-05-22 20:02:58 +02:00
Alexis Mousset
8bfc20506c fix(transport-smtp): Fix transparency codec - 0.9.x (#628)
Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2021-05-22 19:58:27 +02:00
Alexis Mousset
d930c42d50 Prepare 0.9.5 release 2020-11-11 17:39:04 +01:00
Alexis Mousset
1c4a630833 Prepare 0.9.4 release 2020-11-11 17:32:30 +01:00
Alexis Mousset
1738c8e52d fix(transport-sendmail): Stop argument parsing before destination addresses 2020-11-11 17:28:02 +01:00
Alexis Mousset
2b9e476b17 Merge pull request #421 from Zoruk/v0.9.x-fix-attachment
Fix attachment line length
2020-05-06 11:07:22 +02:00
Loïc Haas
44852e42f2 Fix attachment line length 2020-05-06 10:50:36 +02:00
Alexis Mousset
54032b5ce5 fix(builder): lettre_email 0.9.4 2020-04-21 22:05:54 +02:00
Alexis Mousset
6a40f4a5fe fix(builder): Go back to email 0.0.20 2020-04-21 22:04:55 +02:00
Alexis Mousset
a468cb3ab8 chore(all): 0.9.3 release 2020-04-19 10:56:46 +02:00
Alexis Mousset
9b591ff932 chore(all): Fix warnings 2020-04-19 10:24:23 +02:00
Alexis Mousset
eff4e1693f chore(builder): Update email to 0.0.21 2020-04-19 09:56:31 +02:00
Alexis Mousset
75ab05229a feat(all): v0.9.2 release 2019-06-11 19:58:32 +02:00
Alexis Mousset
393ef8dcd1 Simplify header formatting and fix nightly build (fixes #340) 2019-06-11 19:48:01 +02:00
Alexis Mousset
ceb57edfdd Remove failure crate usage (fixes #331) 2019-05-06 09:36:10 +02:00
13 changed files with 227 additions and 132 deletions

View File

@@ -1,3 +1,46 @@
<a name="v0.9.6"></a>
### v0.9.6 (2021-05-22)
#### Bug Fixes
* **transport**
* **SECURITY**: Prevent SMTP command injection in smtp transport
<a name="v0.9.5"></a>
### v0.9.5 (2020-11-11)
#### Bug Fixes
* **transport**
* **SECURITY**: Prevent argument injection in sendmail transport
<a name="v0.9.4"></a>
### v0.9.4 (2020-04-21)
#### Bug Fixes
* **email**
* Go back to `rust-email` 0.0.20 as upgrade broke message formatting ([6a40f4a](https://github.com/lettre/lettre/commit/6a40f4a)
<a name="v0.9.3"></a>
### v0.9.3 (2020-04-19)
#### Bug Fixes
* **all:**
* Fix compilation warnings ([9b591ff](https://github.com/lettre/lettre/commit/9b591ff932e35947f793aaaeec0e3f06e8818449))
* **email**
* Update `rust-email` to 0.0.21 ([eff4e169](https://github.com/lettre/lettre/commit/eff4e1693f5e65430b851707fdfd18046bc796e3))
<a name="v0.9.2"></a>
### v0.9.2 (2019-06-11)
#### Bug Fixes
* **email:**
* Fix compilation with Rust 1.36+ ([393ef8d](https://github.com/lettre/lettre/commit/393ef8dcd1b1c6a6119d0666d5f09b12f50f6b4b))
<a name="v0.9.1"></a>
### v0.9.1 (2019-05-05)

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
version = "0.9.1" # remember to update html_root_url
version = "0.9.6" # remember to update html_root_url
description = "Email client"
readme = "README.md"
homepage = "http://lettre.at"
@@ -28,8 +28,6 @@ hostname = { version = "^0.1", optional = true }
serde = { version = "^1.0", optional = true }
serde_json = { version = "^1.0", optional = true }
serde_derive = { version = "^1.0", optional = true }
failure = "^0.1"
failure_derive = "^0.1"
fast_chemail = "^0.9"
r2d2 = { version = "^0.8", optional = true}

View File

@@ -1,18 +1,35 @@
use failure;
use self::Error::*;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// Error type for email content
#[derive(Fail, Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy)]
pub enum Error {
/// Missing from in envelope
#[fail(display = "missing source address, invalid envelope")]
MissingFrom,
/// Missing to in envelope
#[fail(display = "missing destination address, invalid envelope")]
MissingTo,
/// Invalid email
#[fail(display = "invalid email address")]
InvalidEmailAddress,
}
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(),
})
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
None
}
}
/// Email result type
pub type EmailResult<T> = Result<T, failure::Error>;
pub type EmailResult<T> = Result<T, Error>;

View File

@@ -1,40 +1,61 @@
//! Error and result type for file transport
use failure;
use self::Error::*;
use serde_json;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// An enum of all error kinds.
#[derive(Fail, Debug)]
#[derive(Debug)]
pub enum Error {
/// Internal client error
#[fail(display = "Internal client error: {}", error)]
Client { error: &'static str },
Client(&'static str),
/// IO error
#[fail(display = "IO error: {}", error)]
Io { error: io::Error },
Io(io::Error),
/// JSON serialization error
#[fail(display = "JSON serialization error: {}", error)]
JsonSerialization { error: serde_json::Error },
JsonSerialization(serde_json::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
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 cause(&self) -> Option<&dyn StdError> {
match *self {
Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io { error: err }
Error::Io(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::JsonSerialization { error: err }
Error::JsonSerialization(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client { error: string }
Error::Client(string)
}
}
/// SMTP result type
pub type FileResult = Result<(), failure::Error>;
pub type FileResult = Result<(), Error>;

View File

@@ -3,7 +3,7 @@
//! This mailer contains the available transports for your emails.
//!
#![doc(html_root_url = "https://docs.rs/lettre/0.9.1")]
#![doc(html_root_url = "https://docs.rs/lettre/0.9.6")]
#![deny(
missing_copy_implementations,
trivial_casts,
@@ -30,14 +30,11 @@ extern crate serde;
#[cfg(feature = "serde-impls")]
#[macro_use]
extern crate serde_derive;
extern crate failure;
#[cfg(feature = "file-transport")]
extern crate serde_json;
#[macro_use]
extern crate failure_derive;
extern crate fast_chemail;
#[cfg(feature = "connection-pool")]
extern crate r2d2;
#[cfg(feature = "file-transport")]
extern crate serde_json;
pub mod error;
#[cfg(feature = "file-transport")]
@@ -49,8 +46,7 @@ pub mod smtp;
pub mod stub;
use error::EmailResult;
use error::Error as EmailError;
use failure::Error;
use error::Error;
use fast_chemail::is_valid_email;
#[cfg(feature = "file-transport")]
pub use file::FileTransport;
@@ -77,7 +73,7 @@ pub struct EmailAddress(String);
impl EmailAddress {
pub fn new(address: String) -> EmailResult<EmailAddress> {
if !is_valid_email(&address) && !address.ends_with("localhost") {
Err(EmailError::InvalidEmailAddress)?;
return Err(Error::InvalidEmailAddress);
}
Ok(EmailAddress(address))
}
@@ -127,7 +123,7 @@ impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty.
pub fn new(from: Option<EmailAddress>, to: Vec<EmailAddress>) -> EmailResult<Envelope> {
if to.is_empty() {
Err(EmailError::MissingTo)?;
return Err(Error::MissingTo);
}
Ok(Envelope {
forward_path: to,
@@ -147,7 +143,7 @@ impl Envelope {
}
pub enum Message {
Reader(Box<Read + Send>),
Reader(Box<dyn Read + Send>),
Bytes(Cursor<Vec<u8>>),
}
@@ -179,7 +175,7 @@ impl SendableEmail {
pub fn new_with_reader(
envelope: Envelope,
message_id: String,
message: Box<Read + Send>,
message: Box<dyn Read + Send>,
) -> SendableEmail {
SendableEmail {
envelope,

View File

@@ -1,30 +1,50 @@
//! Error and result type for sendmail transport
use failure;
use self::Error::*;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// An enum of all error kinds.
#[derive(Fail, Debug)]
#[derive(Debug)]
pub enum Error {
/// Internal client error
#[fail(display = "Internal client error: {}", error)]
Client { error: &'static str },
Client(&'static str),
/// IO error
#[fail(display = "IO error: {}", error)]
Io { error: io::Error },
Io(io::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
match *self {
Client(ref err) => err.fmt(fmt),
Io(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
Io(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io { error: err }
Error::Io(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client { error: string }
Error::Client(string)
}
}
/// sendmail result type
pub type SendmailResult = Result<(), failure::Error>;
pub type SendmailResult = Result<(), Error>;

View File

@@ -50,6 +50,7 @@ impl<'a> Transport<'a> for SendmailTransport {
.map(|x| x.as_ref())
.unwrap_or("\"\""),
)
.arg("--")
.args(email.envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -72,9 +73,7 @@ impl<'a> Transport<'a> for SendmailTransport {
Ok(())
} else {
// TODO display stderr
Err(error::Error::Client {
error: "The message could not be sent",
})?
Err(error::Error::Client("The message could not be sent"))
}
}
}

View File

@@ -51,7 +51,15 @@ impl ClientCodec {
match self.escape_count {
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
2 => self.escape_count = if *byte == b'.' { 3 } else { 0 },
2 => {
self.escape_count = if *byte == b'.' {
3
} else if *byte == b'\r' {
1
} else {
0
}
}
_ => unreachable!(),
}
if self.escape_count == 3 {
@@ -193,7 +201,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
}
/// Sends the message content
pub fn message(&mut self, message: Box<Read>) -> SmtpResult {
pub fn message(&mut self, message: Box<dyn Read>) -> SmtpResult {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new();
@@ -286,6 +294,7 @@ mod test {
let mut buf: Vec<u8> = vec![];
assert!(codec.encode(b"test\r\n", &mut buf).is_ok());
assert!(codec.encode(b"test\r\n\r\n", &mut buf).is_ok());
assert!(codec.encode(b".\r\n", &mut buf).is_ok());
assert!(codec.encode(b"\r\ntest", &mut buf).is_ok());
assert!(codec.encode(b"te\r\n.\r\nst", &mut buf).is_ok());
@@ -296,7 +305,7 @@ mod test {
assert!(codec.encode(b"test", &mut buf).is_ok());
assert_eq!(
String::from_utf8(buf).unwrap(),
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
"test\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
);
}

View File

@@ -245,7 +245,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
};

View File

@@ -42,36 +42,31 @@ pub enum Error {
impl Display for Error {
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),
Tls(ref err) => err.fmt(fmt),
Parsing(ref err) => fmt.write_str(err.description()),
}
}
}
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(),
Tls(ref err) => err.description(),
Parsing(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&StdError> {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
ChallengeParsing(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre_email"
version = "0.9.1" # remember to update html_root_url
version = "0.9.4" # remember to update html_root_url
description = "Email builder"
readme = "README.md"
homepage = "http://lettre.at"
@@ -29,5 +29,3 @@ time = "^0.1"
uuid = { version = "^0.7", features = ["v4"] }
lettre = { version = "^0.9", path = "../lettre", default-features = false }
base64 = "^0.10"
failure = "^0.1"
failure_derive = "^0.1"

View File

@@ -2,35 +2,52 @@
use lettre;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
use self::Error::*;
/// An enum of all error kinds.
#[derive(Debug, Fail)]
#[derive(Debug)]
pub enum Error {
/// Envelope error
#[fail(display = "lettre error: {}", error)]
Envelope {
/// inner error
error: lettre::error::Error,
},
Envelope(lettre::error::Error),
/// Unparseable filename for attachment
#[fail(display = "the attachment filename could not be parsed")]
CannotParseFilename,
/// IO error
#[fail(display = "IO error: {}", error)]
Io {
/// inner error
error: 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 { error: err }
Error::Io(err)
}
}
impl From<lettre::error::Error> for Error {
fn from(err: lettre::error::Error) -> Error {
Error::Envelope { error: err }
Error::Envelope(err)
}
}

View File

@@ -1,7 +1,7 @@
//! Lettre is a mailer written in Rust. lettre_email provides a simple email builder.
//!
#![doc(html_root_url = "https://docs.rs/lettre_email/0.9.1")]
#![doc(html_root_url = "https://docs.rs/lettre_email/0.9.4")]
#![deny(
missing_docs,
missing_debug_implementations,
@@ -13,22 +13,17 @@
unused_import_braces
)]
extern crate failure;
#[macro_use]
extern crate failure_derive;
extern crate base64;
extern crate email as email_format;
extern crate lettre;
pub extern crate mime;
extern crate time;
extern crate uuid;
pub extern crate mime;
pub mod error;
pub use email_format::{Address, Header, Mailbox, MimeMessage, MimeMultipartType};
use error::Error as EmailError;
use failure::Error;
use error::Error;
use lettre::{error::Error as LettreError, EmailAddress, Envelope, SendableEmail};
use mime::Mime;
use std::fs;
@@ -136,7 +131,7 @@ impl PartBuilder {
/// Adds a `ContentType` header with the given MIME type
pub fn content_type(self, content_type: &Mime) -> PartBuilder {
self.header(("Content-Type", format!("{}", content_type).as_ref()))
self.header(("Content-Type", content_type.to_string()))
}
/// Adds a child part
@@ -263,7 +258,7 @@ impl EmailBuilder {
filename.unwrap_or(
path.file_name()
.and_then(|x| x.to_str())
.ok_or(EmailError::CannotParseFilename)?,
.ok_or(Error::CannotParseFilename)?,
),
content_type,
)
@@ -276,7 +271,13 @@ impl EmailBuilder {
filename: &str,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
let encoded_body = base64::encode(&body);
let encoded_body = base64::encode(&body)
.as_bytes()
.chunks(72)
// base64 encoding is guaranteed to return utf-8, so this won't panic
.map(|s| std::str::from_utf8(s).unwrap())
.collect::<Vec<_>>()
.join("\r\n");
let content = PartBuilder::new()
.body(encoded_body)
.header((
@@ -306,10 +307,7 @@ impl EmailBuilder {
pub fn text<S: Into<String>>(self, body: S) -> EmailBuilder {
let text = PartBuilder::new()
.body(body)
.header((
"Content-Type",
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref(),
))
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
.build();
self.child(text)
}
@@ -318,10 +316,7 @@ impl EmailBuilder {
pub fn html<S: Into<String>>(self, body: S) -> EmailBuilder {
let html = PartBuilder::new()
.body(body)
.header((
"Content-Type",
format!("{}", mime::TEXT_HTML_UTF_8).as_ref(),
))
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
.build();
self.child(html)
}
@@ -334,18 +329,12 @@ impl EmailBuilder {
) -> EmailBuilder {
let text = PartBuilder::new()
.body(body_text)
.header((
"Content-Type",
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref(),
))
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
.build();
let html = PartBuilder::new()
.body(body_html)
.header((
"Content-Type",
format!("{}", mime::TEXT_HTML_UTF_8).as_ref(),
))
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
.build();
let alternate = PartBuilder::new()
@@ -384,7 +373,7 @@ impl EmailBuilder {
}
// Add the sender header, if any.
if let Some(ref v) = self.sender {
self.message = self.message.header(("Sender", v.to_string().as_ref()));
self.message = self.message.header(("Sender", v.to_string()));
}
// Calculate the envelope
let envelope = match self.envelope {
@@ -404,7 +393,7 @@ impl EmailBuilder {
}
}
let from = Some(EmailAddress::from_str(&match self.sender {
Some(x) => Ok(x.address.clone()), // if we have a sender_header, use it
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
@@ -416,15 +405,11 @@ impl EmailBuilder {
// 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(EmailError::Envelope {
error: LettreError::MissingFrom,
}), // empty envelope sender
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
},
},
// if we don't have a from header
None => Err(EmailError::Envelope {
error: LettreError::MissingFrom,
}), // empty envelope sender
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
}
}
}?)?);
@@ -448,9 +433,7 @@ impl EmailBuilder {
.message
.header(Header::new_with_value("From".into(), from).unwrap());
} else {
Err(EmailError::Envelope {
error: LettreError::MissingFrom,
})?;
return Err(Error::Envelope(LettreError::MissingFrom));
}
if !self.cc.is_empty() {
self.message = self
@@ -476,7 +459,7 @@ impl EmailBuilder {
if !self.date_issued {
self.message = self
.message
.header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
.header(("Date", Tm::rfc822z(&now()).to_string()));
}
self.message = self.message.header(("MIME-Version", "1.0"));
@@ -602,5 +585,4 @@ mod test {
.as_slice()
);
}
}