Compare commits

..

36 Commits

Author SHA1 Message Date
Alexis Mousset
58026d8318 Prepare 0.8.4 release 2020-11-11 17:46:42 +01:00
Alexis Mousset
d86900e37e fix(transport-sendmail): Stop argument parsing before destination addresses 2020-11-11 17:43:05 +01:00
Alexis Mousset
9dadc4d6eb Merge pull request #302 from kper/v0.8.x
Add `Reference` and `In-Reply-To` to the email header.
2018-09-20 20:26:35 +02:00
Alexis Mousset
bb7815a366 fix(email): Do not include Bcc addresses in headers 2018-09-20 20:23:10 +02:00
Alexis Mousset
b24c0a367d Merge branch 'v0.8.x' into v0.8.x 2018-09-20 20:13:25 +02:00
Alexis Mousset
e0637c9ed4 Merge pull request #307 from sameer/v0.8.x
fix(transport-smtp) Use nom 4.0 to fix build errors in #298
2018-09-12 10:25:43 +02:00
Jarred Nicholls
6c3de0e85b fix(transport-smtp): Use nom 4.0 to fix build errors in #298 2018-08-14 17:42:26 -04:00
Kevin Per
692f8a5346 Add Reference and In-Reply-To to the email header.
This is patch makes it possible to answer emails in reference to their parents and therefore it is possible for gmail to group them.
The `Reply-To` header attribute alone didn't work.
2018-07-05 22:37:37 +02:00
Alexis Mousset
60f06f1682 docs(all): Add an example of custom tls settings 2018-05-20 13:28:47 +02:00
Alexis Mousset
29b130c919 docs(all): README should not use external crates 2018-05-20 00:04:28 +02:00
Alexis Mousset
247dc0dc16 Merge pull request #277 from amousset/v0.8.x
Prepare 0.8.2
2018-05-04 00:05:03 +02:00
Alexis Mousset
19d32e7574 Build doc from v0.8.x branch 2018-05-03 23:52:33 +02:00
Alexis Mousset
85c8472314 Prepare 0.8.2 release 2018-05-03 23:51:16 +02:00
dimlev
cc3580a894 fix(transport): Write timeout is not set in smtp transport 2018-05-03 23:48:27 +02:00
Alexis Mousset
0cf018a85e feat(transport): Use md-5 and hmac instead of rust-crypto
RustCrypto is not supported anymore, and this avoids
compiling useless code.
2018-05-03 23:47:16 +02:00
Alexis Mousset
57bbabaa6a feat(all): Add set -xe option to build scripts 2018-05-03 23:47:16 +02:00
Alexis Mousset
4122e5b84c Update .travis.yml 2018-05-03 23:47:16 +02:00
Alexis Mousset
a0732b5e31 Update .travis.yml 2018-05-03 23:47:16 +02:00
Alexis Mousset
7223929b05 Update .travis.yml 2018-05-03 23:47:16 +02:00
Alexis Mousset
95a0438165 Update .travis.yml 2018-05-03 23:47:16 +02:00
Alexis Mousset
5c26d6ea67 Update .travis.yml 2018-05-03 23:47:16 +02:00
Alexis Mousset
3177b58c6d feat(all): Move post-success scripts to separate files 2018-05-03 23:47:16 +02:00
Alexis Mousset
360c42ffb8 style(all): Fix typos 2018-05-03 23:47:16 +02:00
Alexis Mousset
3c76d5ac68 Update .travis.yml 2018-05-03 23:47:16 +02:00
Alexis Mousset
f589f3006d Update .travis.yml 2018-05-03 23:47:16 +02:00
Alexis Mousset
a5294df637 feat(all): Add website upload to travis build script 2018-05-03 23:47:16 +02:00
Alexis Mousset
7a9b99f89a Remove doc redirect to lettre.at 2018-05-03 23:47:16 +02:00
Alexis Mousset
b4eaa91b4f Delete CNAME 2018-05-03 23:47:16 +02:00
Alexis Mousset
a03bfa0085 feat(all): Add codecov upload in travis 2018-05-03 23:47:16 +02:00
Alexis Mousset
1ebbe660f5 feat(all): Update README to put useful links at the top 2018-05-03 23:47:16 +02:00
Alexis Mousset
f7ee5c427a feat(all): Update badges in README and Cargo.toml 2018-05-03 23:46:10 +02:00
Alexis Mousset
5f2be2b73e Create CNAME 2018-05-03 23:46:10 +02:00
Alexis Mousset
27935e32ef feat(all): Move docs from hugo to gitbook
Also move generated html to the lettre.github.io repo
2018-05-03 23:46:10 +02:00
Alexis Mousset
73d823c8ca Merge pull request #255 from amousset/prepare-0.8.1
Prepare 0.8.1
2018-04-11 23:36:46 +02:00
Alexis Mousset
d45ee40f4a fix(all): No cache on build folders 2018-04-11 23:28:03 +02:00
Alexis Mousset
e295c5db5e Prepare 0.8.1 release 2018-04-11 22:54:29 +02:00
54 changed files with 1571 additions and 1361 deletions

View File

@@ -4,7 +4,7 @@ set -xe
cd website
make clean && make
echo "lettre.at" > _book/html/CNAME
echo "lettre.at" > _book/CNAME
sudo pip install ghp-import
ghp-import -n _book/html
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
ghp-import -n _book
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages

View File

@@ -1,21 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Code allowing to reproduce the bug.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- Lettre version
- OS
**Additional context**
Add any other context about the problem here.

View File

@@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -3,7 +3,7 @@ rust:
- stable
- beta
- nightly
- 1.32.0
- 1.20.0
matrix:
allow_failures:
- rust: nightly
@@ -19,11 +19,13 @@ addons:
- gcc
- binutils-dev
- libiberty-dev
- npm
before_script:
- smtp-sink 2525 1000&
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
- sudo npm set strict-ssl false && sudo npm install -g gitbook-cli
script:
- cargo test --verbose --all --all-features
- cargo test --verbose --all
after_success:
- ./.build-scripts/codecov.sh
- '[ "$TRAVIS_RUST_VERSION" = "stable" ] && [ "$TRAVIS_BRANCH" = "v0.9.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'
- '[ "$TRAVIS_BRANCH" = "v0.8.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'

View File

@@ -1,49 +1,11 @@
<a name="v0.9.2"></a>
### v0.9.2 (2019-06-11)
<a name="v0.8.4"></a>
### v0.8.4 (2020-11-11)
#### Bug Fixes
* **email:**
* Fix compilation with Rust 1.36+ ([393ef8d](https://github.com/lettre/lettre/commit/393ef8dcd1b1c6a6119d0666d5f09b12f50f6b4b))
* **transport**
<a name="v0.9.1"></a>
### v0.9.1 (2019-05-05)
#### Features
* **email:**
* Re-export mime crate ([a0c8fb9](https://github.com/lettre/lettre/commit/a0c8fb9))
<a name="v0.9.0"></a>
### v0.9.0 (2019-03-17)
#### Bug Fixes
* **email:**
* Inserting 'from' from envelope into message headers ([058fa69](https://github.com/lettre/lettre/commit/058fa69))
* Do not include Bcc addresses in headers ([ee31bbe](https://github.com/lettre/lettre/commit/ee31bbe))
* **transport:**
* Write timeout is not set in smtp transport ([d71b560](https://github.com/lettre/lettre/commit/d71b560))
* Client::read_response infinite loop ([72f3cd8](https://github.com/lettre/lettre/commit/72f3cd8))
#### Features
* **all:**
* Update dependencies
* Start using the failure crate for errors ([c10fe3d](https://github.com/lettre/lettre/commit/c10fe3d))
* **transport:**
* Remove TLS 1.1 in accepted protocols by default (only allow TLS 1.2) ([4b48bdb](https://github.com/lettre/lettre/commit/4b48bdb))
* Initial support for XOAUTH2 ([ed7c164](https://github.com/lettre/lettre/commit/ed7c164))
* Remove support for CRAM-MD5 ([bc09aa2](https://github.com/lettre/lettre/commit/bc09aa2))
* SMTP connection pool implementation with r2d2 ([434654e](https://github.com/lettre/lettre/commit/434654e))
* Use md-5 and hmac instead of rust-crypto ([e7e0f34](https://github.com/lettre/lettre/commit/e7e0f34))
* Gmail transport simple example ([a8d8e2a](https://github.com/lettre/lettre/commit/a8d8e2a))
* **email:**
* Add In-Reply-To and References headers ([fc91bb6](https://github.com/lettre/lettre/commit/fc91bb6))
* Remove non-chaining builder methods ([1baf8a9](https://github.com/lettre/lettre/commit/1baf8a9))
* **SECURITY**: Prevent argument injection in sendmail transport
<a name="v0.8.2"></a>
### v0.8.2 (2018-05-03)

View File

@@ -34,13 +34,13 @@ Lettre provides the following features:
## Example
This library requires Rust 1.32 or newer.
This library requires Rust 1.20 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.9"
lettre_email = "0.9"
lettre = "0.8"
lettre_email = "0.8"
```
```rust,no_run

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
version = "0.9.2" # remember to update html_root_url
version = "0.8.4" # remember to update html_root_url
description = "Email client"
readme = "README.md"
homepage = "http://lettre.at"
@@ -22,32 +22,29 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
log = "^0.4"
nom = { version = "^4.0", optional = true }
bufstream = { version = "^0.1", optional = true }
native-tls = { version = "^0.2", optional = true }
base64 = { version = "^0.10", optional = true }
native-tls = { version = "^0.1", optional = true }
base64 = { version = "^0.9", optional = true }
hex = { version = "^0.3", optional = true }
hostname = { version = "^0.1", optional = true }
md-5 = { version = "^0.7", optional = true }
hmac = { version = "^0.6", optional = true }
serde = { version = "^1.0", optional = true }
serde_json = { version = "^1.0", optional = true }
serde_derive = { version = "^1.0", optional = true }
fast_chemail = "^0.9"
r2d2 = { version = "^0.8", optional = true}
[dev-dependencies]
env_logger = "^0.6"
glob = "0.3"
env_logger = "^0.5"
glob = "0.2"
[features]
default = ["file-transport", "smtp-transport", "sendmail-transport"]
default = ["file-transport", "crammd5-auth", "smtp-transport", "sendmail-transport"]
unstable = []
serde-impls = ["serde", "serde_derive"]
file-transport = ["serde-impls", "serde_json"]
crammd5-auth = ["md-5", "hmac", "hex"]
smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"]
sendmail-transport = []
connection-pool = [ "r2d2" ]
[[example]]
name = "smtp"
required-features = ["smtp-transport"]
[[example]]
name = "smtp_gmail"
required-features = ["smtp-transport"]

View File

@@ -3,9 +3,9 @@
extern crate lettre;
extern crate test;
use lettre::{ClientSecurity, SmtpTransport};
use lettre::{EmailAddress, EmailTransport, SimpleSendableEmail};
use lettre::smtp::ConnectionReuseParameters;
use lettre::{ClientSecurity, Envelope, SmtpTransport};
use lettre::{EmailAddress, SendableEmail, Transport};
#[bench]
fn bench_simple_send(b: &mut test::Bencher) {
@@ -13,16 +13,13 @@ fn bench_simple_send(b: &mut test::Bencher) {
.unwrap()
.build();
b.iter(|| {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
let email = SimpleSendableEmail::new(
EmailAddress::new("user@localhost".to_string()),
vec![EmailAddress::new("root@localhost".to_string())],
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
"Hello world".to_string(),
);
let result = sender.send(email);
let result = sender.send(&email);
assert!(result.is_ok());
});
}
@@ -34,16 +31,13 @@ fn bench_reuse_send(b: &mut test::Bencher) {
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.build();
b.iter(|| {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
let email = SimpleSendableEmail::new(
EmailAddress::new("user@localhost".to_string()),
vec![EmailAddress::new("root@localhost".to_string())],
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
"Hello world".to_string(),
);
let result = sender.send(email);
let result = sender.send(&email);
assert!(result.is_ok());
});
sender.close()

View File

@@ -1,25 +1,24 @@
extern crate env_logger;
extern crate lettre;
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
use lettre::{EmailTransport, SimpleSendableEmail, SmtpTransport};
fn main() {
env_logger::init();
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"my-message-id".to_string(),
"Hello ß☺ example".to_string(),
).unwrap();
// Open a local connection on port 25
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
let mut mailer = SmtpTransport::builder_unencrypted_localhost()
.unwrap()
.build();
// Send the email
let result = mailer.send(email);
let result = mailer.send(&email);
if result.is_ok() {
println!("Email sent");

View File

@@ -1,38 +0,0 @@
extern crate lettre;
use lettre::smtp::authentication::Credentials;
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
fn main() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("from@gmail.com".to_string()).unwrap()),
vec![EmailAddress::new("to@example.com".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello example".to_string().into_bytes(),
);
let creds = Credentials::new(
"example_username".to_string(),
"example_password".to_string(),
);
// Open a remote connection to gmail
let mut mailer = SmtpClient::new_simple("smtp.gmail.com")
.unwrap()
.credentials(creds)
.transport();
// Send the email
let result = mailer.send(email);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}

View File

@@ -1,35 +0,0 @@
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
use self::Error::*;
/// Error type for email content
#[derive(Debug, Clone, Copy)]
pub enum Error {
/// Missing from in envelope
MissingFrom,
/// Missing to in envelope
MissingTo,
/// Invalid email
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, Error>;

View File

@@ -1,12 +1,10 @@
//! Error and result type for file transport
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
use serde_json;
use std::io;
use self::Error::*;
use serde_json;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
/// An enum of all error kinds.
#[derive(Debug)]
@@ -45,19 +43,19 @@ impl StdError for Error {
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
Io(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::JsonSerialization(err)
JsonSerialization(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client(string)
Client(string)
}
}

View File

@@ -3,58 +3,52 @@
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
//!
use EmailTransport;
use SendableEmail;
use SimpleSendableEmail;
use file::error::FileResult;
use serde_json;
use std::fs::File;
use std::io::Read;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use Envelope;
use SendableEmail;
use Transport;
pub mod error;
/// Writes the content and the envelope information to a file
#[derive(Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct FileTransport {
pub struct FileEmailTransport {
path: PathBuf,
}
impl FileTransport {
impl FileEmailTransport {
/// Creates a new transport to the given directory
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
FileTransport {
path: PathBuf::from(path.as_ref()),
}
pub fn new<P: AsRef<Path>>(path: P) -> FileEmailTransport {
let mut path_buf = PathBuf::new();
path_buf.push(path);
FileEmailTransport { path: path_buf }
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
struct SerializableEmail {
envelope: Envelope,
message_id: String,
message: Vec<u8>,
}
impl<'a> Transport<'a> for FileTransport {
type Result = FileResult;
fn send(&mut self, email: SendableEmail) -> FileResult {
let message_id = email.message_id().to_string();
let envelope = email.envelope().clone();
impl<'a, T: Read + 'a> EmailTransport<'a, T, FileResult> for FileEmailTransport {
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> FileResult {
let mut file = self.path.clone();
file.push(format!("{}.json", message_id));
file.push(format!("{}.txt", email.message_id()));
let serialized = serde_json::to_string(&SerializableEmail {
envelope,
message_id,
message: email.message_to_string()?.as_bytes().to_vec(),
})?;
let mut f = File::create(file.as_path())?;
let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content);
let simple_email = SimpleSendableEmail::new_with_envelope(
email.envelope().clone(),
email.message_id().to_string(),
message_content,
);
f.write_all(serde_json::to_string(&simple_email)?.as_bytes())?;
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
Ok(())
}
}

View File

@@ -1,80 +1,102 @@
//! 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.
//! This mailer contains the available transports for your emails. To be sendable, the
//! emails have to implement `SendableEmail`.
//!
#![doc(html_root_url = "https://docs.rs/lettre/0.9.2")]
#![deny(
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces
)]
#![doc(html_root_url = "https://docs.rs/lettre/0.8.4")]
#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts,
trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces,
unused_qualifications)]
#[cfg(feature = "smtp-transport")]
extern crate base64;
#[cfg(feature = "smtp-transport")]
extern crate bufstream;
#[cfg(feature = "crammd5-auth")]
extern crate hex;
#[cfg(feature = "crammd5-auth")]
extern crate hmac;
#[cfg(feature = "smtp-transport")]
extern crate hostname;
#[macro_use]
extern crate log;
#[cfg(feature = "crammd5-auth")]
extern crate md5;
#[cfg(feature = "smtp-transport")]
extern crate native_tls;
#[cfg(feature = "smtp-transport")]
#[macro_use]
extern crate nom;
#[cfg(feature = "serde-impls")]
extern crate serde;
#[cfg(feature = "serde-impls")]
#[macro_use]
extern crate serde_derive;
#[cfg(feature = "file-transport")]
extern crate serde_json;
extern crate fast_chemail;
#[cfg(feature = "connection-pool")]
extern crate r2d2;
pub mod error;
#[cfg(feature = "file-transport")]
pub mod file;
#[cfg(feature = "sendmail-transport")]
pub mod sendmail;
#[cfg(feature = "smtp-transport")]
pub mod smtp;
#[cfg(feature = "sendmail-transport")]
pub mod sendmail;
pub mod stub;
use error::EmailResult;
use error::Error;
use fast_chemail::is_valid_email;
#[cfg(feature = "file-transport")]
pub use file::FileTransport;
pub mod file;
#[cfg(feature = "file-transport")]
pub use file::FileEmailTransport;
#[cfg(feature = "sendmail-transport")]
pub use sendmail::SendmailTransport;
#[cfg(feature = "smtp-transport")]
pub use smtp::client::net::ClientTlsParameters;
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
pub use smtp::r2d2::SmtpConnectionManager;
pub use smtp::{ClientSecurity, SmtpTransport};
#[cfg(feature = "smtp-transport")]
pub use smtp::{ClientSecurity, SmtpClient, SmtpTransport};
use std::ffi::OsStr;
pub use smtp::client::net::ClientTlsParameters;
use std::fmt::{self, Display, Formatter};
use std::io;
use std::io::Cursor;
use std::io::Read;
use std::error::Error as StdError;
use std::str::FromStr;
/// Error type for email content
#[derive(Debug, Clone, Copy)]
pub enum Error {
/// Missing from in envelope
MissingFrom,
/// Missing to in envelope
MissingTo,
/// Invalid email
InvalidEmailAddress,
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Error::MissingFrom => "missing source address, invalid envelope",
Error::MissingTo => "missing destination address, invalid envelope",
Error::InvalidEmailAddress => "invalid email address",
}
}
fn cause(&self) -> Option<&StdError> {
None
}
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
/// Email result type
pub type EmailResult<T> = Result<T, Error>;
/// Email address
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct EmailAddress(String);
impl EmailAddress {
/// Creates a new `EmailAddress`. For now it makes no validation.
pub fn new(address: String) -> EmailResult<EmailAddress> {
if !is_valid_email(&address) && !address.ends_with("localhost") {
Err(Error::InvalidEmailAddress)?;
}
// TODO make some basic sanity checks
Ok(EmailAddress(address))
}
}
@@ -93,18 +115,6 @@ impl Display for EmailAddress {
}
}
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()
}
}
/// Simple email envelope representation
///
/// We only accept mailboxes, and do not support source routes (as per RFC).
@@ -123,7 +133,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(Error::MissingTo)?;
return Err(Error::MissingTo);
}
Ok(Envelope {
forward_path: to,
@@ -140,74 +150,130 @@ impl Envelope {
pub fn from(&self) -> Option<&EmailAddress> {
self.reverse_path.as_ref()
}
}
pub enum Message {
Reader(Box<Read + Send>),
Bytes(Cursor<Vec<u8>>),
}
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),
}
/// Creates a new builder
pub fn builder() -> EnvelopeBuilder {
EnvelopeBuilder::new()
}
}
/// Sendable email structure
pub struct SendableEmail {
envelope: Envelope,
message_id: String,
message: Message,
/// Simple email envelope representation
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct EnvelopeBuilder {
/// The envelope recipients' addresses
to: Vec<EmailAddress>,
/// The envelope sender address
from: Option<EmailAddress>,
}
impl SendableEmail {
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
SendableEmail {
envelope,
message_id,
message: Message::Bytes(Cursor::new(message)),
impl EnvelopeBuilder {
/// Constructs an envelope with no recipients and an empty sender
pub fn new() -> Self {
EnvelopeBuilder {
to: vec![],
from: None,
}
}
pub fn new_with_reader(
envelope: Envelope,
message_id: String,
message: Box<Read + Send>,
) -> SendableEmail {
SendableEmail {
envelope,
message_id,
message: Message::Reader(message),
}
/// Adds a recipient
pub fn to<S: Into<EmailAddress>>(mut self, address: S) -> Self {
self.add_to(address);
self
}
pub fn envelope(&self) -> &Envelope {
&self.envelope
/// Adds a recipient
pub fn add_to<S: Into<EmailAddress>>(&mut self, address: S) {
self.to.push(address.into());
}
pub fn message_id(&self) -> &str {
&self.message_id
/// Sets the sender
pub fn from<S: Into<EmailAddress>>(mut self, address: S) -> Self {
self.set_from(address);
self
}
pub fn message(self) -> Message {
self.message
/// Sets the sender
pub fn set_from<S: Into<EmailAddress>>(&mut self, address: S) {
self.from = Some(address.into());
}
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)
/// Build the envelope
pub fn build(self) -> EmailResult<Envelope> {
Envelope::new(self.from, self.to)
}
}
/// Email sendable by an SMTP client
pub trait SendableEmail<'a, T: Read + 'a> {
/// Envelope
fn envelope(&self) -> Envelope;
/// Message ID, used for logging
fn message_id(&self) -> String;
/// Message content
fn message(&'a self) -> Box<T>;
}
/// Transport method for emails
pub trait Transport<'a> {
/// Result type for the transport
type Result;
pub trait EmailTransport<'a, U: Read + 'a, V> {
/// Sends the email
fn send(&mut self, email: SendableEmail) -> Self::Result;
fn send<T: SendableEmail<'a, U> + 'a>(&mut self, email: &'a T) -> V;
}
/// Minimal email structure
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct SimpleSendableEmail {
/// Envelope
envelope: Envelope,
/// Message ID
message_id: String,
/// Message content
message: Vec<u8>,
}
impl SimpleSendableEmail {
/// Returns a new email
pub fn new(
from_address: String,
to_addresses: &[String],
message_id: String,
message: String,
) -> EmailResult<SimpleSendableEmail> {
let to: Result<Vec<EmailAddress>, Error> = to_addresses
.iter()
.map(|x| EmailAddress::new(x.clone()))
.collect();
Ok(SimpleSendableEmail::new_with_envelope(
Envelope::new(Some(EmailAddress::new(from_address)?), to?)?,
message_id,
message,
))
}
/// Returns a new email from a valid envelope
pub fn new_with_envelope(
envelope: Envelope,
message_id: String,
message: String,
) -> SimpleSendableEmail {
SimpleSendableEmail {
envelope,
message_id,
message: message.into_bytes(),
}
}
}
impl<'a> SendableEmail<'a, &'a [u8]> for SimpleSendableEmail {
fn envelope(&self) -> Envelope {
self.envelope.clone()
}
fn message_id(&self) -> String {
self.message_id.clone()
}
fn message(&'a self) -> Box<&[u8]> {
Box::new(self.message.as_slice())
}
}

View File

@@ -1,10 +1,8 @@
//! Error and result type for sendmail transport
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
use self::Error::*;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
/// An enum of all error kinds.
@@ -40,13 +38,13 @@ impl StdError for Error {
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
Io(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client(string)
Client(string)
}
}

View File

@@ -1,12 +1,11 @@
//! The sendmail transport sends the email using the local sendmail command.
//!
use {EmailTransport, SendableEmail};
use sendmail::error::SendmailResult;
use std::io::prelude::*;
use std::io::Read;
use std::io::prelude::*;
use std::process::{Command, Stdio};
use SendableEmail;
use Transport;
pub mod error;
@@ -33,24 +32,23 @@ impl SendmailTransport {
}
}
impl<'a> Transport<'a> for SendmailTransport {
type Result = SendmailResult;
fn send(&mut self, email: SendableEmail) -> SendmailResult {
let message_id = email.message_id().to_string();
impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTransport {
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SendmailResult {
let envelope = email.envelope();
// Spawn the sendmail command
let to_addresses: Vec<String> = envelope.to().iter().map(|x| x.to_string()).collect();
let mut process = Command::new(&self.command)
.arg("-i")
.arg("-f")
.arg(
email
.envelope()
.from()
.map(|x| x.as_ref())
.unwrap_or("\"\""),
)
.args(email.envelope.to())
.args(&[
"-i",
"-f",
&match envelope.from() {
Some(address) => address.to_string(),
None => "\"\"".to_string(),
},
"--",
&to_addresses.join(" "),
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
@@ -58,21 +56,26 @@ impl<'a> Transport<'a> for SendmailTransport {
let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content);
process
match process
.stdin
.as_mut()
.unwrap()
.write_all(message_content.as_bytes())?;
.write_all(message_content.as_bytes())
{
Ok(_) => (),
Err(error) => return Err(From::from(error)),
}
info!("Wrote {} message to stdin", message_id);
info!("Wrote message to stdin");
let output = process.wait_with_output()?;
if output.status.success() {
Ok(())
if let Ok(output) = process.wait_with_output() {
if output.status.success() {
Ok(())
} else {
Err(From::from("The message could not be sent"))
}
} else {
// TODO display stderr
Err(error::Error::Client("The message could not be sent"))?
Err(From::from("The sendmail process stopped"))
}
}
}

View File

@@ -1,13 +1,31 @@
//! Provides limited SASL authentication mechanisms
//! Provides authentication mechanisms
#[cfg(feature = "crammd5-auth")]
use md5::Md5;
#[cfg(feature = "crammd5-auth")]
use hmac::{Hmac, Mac};
#[cfg(feature = "crammd5-auth")]
use hex;
use smtp::NUL;
use smtp::error::Error;
use std::fmt::{self, Display, Formatter};
/// Accepted authentication mechanisms on an encrypted connection
/// Trying LOGIN last as it is deprecated.
#[cfg(feature = "crammd5-auth")]
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] =
&[Mechanism::Plain, Mechanism::CramMd5, Mechanism::Login];
/// Accepted authentication mechanisms on an encrypted connection
/// Trying LOGIN last as it is deprecated.
#[cfg(not(feature = "crammd5-auth"))]
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Accepted authentication mechanisms on an unencrypted connection
#[cfg(feature = "crammd5-auth")]
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::CramMd5];
/// Accepted authentication mechanisms on an unencrypted connection
/// When CRAMMD5 support is not enabled, no mechanisms are allowed.
#[cfg(not(feature = "crammd5-auth"))]
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
/// Convertable to user credentials
@@ -33,17 +51,14 @@ impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Credentials {
authentication_identity: String,
secret: String,
username: String,
password: String,
}
impl Credentials {
/// Create a `Credentials` struct from username and password
pub fn new(username: String, password: String) -> Credentials {
Credentials {
authentication_identity: username,
secret: password,
}
Credentials { username, password }
}
}
@@ -58,9 +73,10 @@ pub enum Mechanism {
/// Obsolete but needed for some providers (like office365)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
Login,
/// Non-standard XOAUTH2 mechanism
/// https://developers.google.com/gmail/imap/xoauth2-protocol
Xoauth2,
/// CRAM-MD5 authentication mechanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195
#[cfg(feature = "crammd5-auth")]
CramMd5,
}
impl Display for Mechanism {
@@ -71,7 +87,8 @@ impl Display for Mechanism {
match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
Mechanism::Xoauth2 => "XOAUTH2",
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => "CRAM-MD5",
}
)
}
@@ -79,49 +96,64 @@ impl Display for Mechanism {
impl Mechanism {
/// Does the mechanism supports initial response
pub fn supports_initial_response(self) -> bool {
match self {
Mechanism::Plain | Mechanism::Xoauth2 => true,
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
pub fn supports_initial_response(&self) -> bool {
match *self {
Mechanism::Plain => true,
Mechanism::Login => false,
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => false,
}
}
/// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases
pub fn response(
self,
&self,
credentials: &Credentials,
challenge: Option<&str>,
) -> Result<String, Error> {
match self {
match *self {
Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret
"{}{}{}{}",
NUL, credentials.username, NUL, credentials.password
)),
},
Mechanism::Login => {
let decoded_challenge =
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
let decoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(credentials.authentication_identity.to_string());
return Ok(credentials.username.to_string());
}
if vec!["Password", "Password:"].contains(&decoded_challenge) {
return Ok(credentials.secret.to_string());
return Ok(credentials.password.to_string());
}
Err(Error::Client("Unrecognized challenge"))
}
Mechanism::Xoauth2 => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"user={}\x01auth=Bearer {}\x01\x01",
credentials.authentication_identity, credentials.secret
)),
},
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => {
let decoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
let mut hmac: Hmac<Md5> = Hmac::new_varkey(credentials.password.as_bytes())
.expect("md5 should support variable key size");
hmac.input(decoded_challenge.as_bytes());
Ok(format!(
"{} {}",
credentials.username,
hex::encode(hmac.result().code())
))
}
}
}
}
@@ -161,18 +193,21 @@ mod test {
}
#[test]
fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2;
#[cfg(feature = "crammd5-auth")]
fn test_cram_md5() {
let mechanism = Mechanism::CramMd5;
let credentials = Credentials::new(
"username".to_string(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
);
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
assert_eq!(
mechanism.response(&credentials, None).unwrap(),
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
mechanism
.response(
&credentials,
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")
)
.unwrap(),
"alice a540ebe4ef2304070bbc3c456c1f64c0"
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
assert!(mechanism.response(&credentials, None).is_err());
}
}

View File

@@ -2,6 +2,7 @@
use bufstream::BufStream;
use nom::ErrorKind as NomErrorKind;
use smtp::{CRLF, MESSAGE_ENDING};
use smtp::authentication::{Credentials, Mechanism};
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
use smtp::commands::*;
@@ -13,8 +14,8 @@ use std::net::ToSocketAddrs;
use std::string::String;
use std::time::Duration;
pub mod mock;
pub mod net;
pub mod mock;
/// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)]
@@ -71,12 +72,12 @@ impl ClientCodec {
/// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays
fn escape_crlf(string: &str) -> String {
string.replace("\r\n", "<CRLF>")
string.replace(CRLF, "<CRLF>")
}
/// Structure that implements the SMTP client
#[derive(Debug, Default)]
pub struct InnerClient<S: Write + Read = NetworkStream> {
pub struct Client<S: Write + Read = NetworkStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<BufStream<S>>,
@@ -88,17 +89,17 @@ macro_rules! return_err (
})
);
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default_derive))]
impl<S: Write + Read> InnerClient<S> {
#[cfg_attr(feature = "cargo-clippy", allow(new_without_default_derive))]
impl<S: Write + Read> Client<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn new() -> InnerClient<S> {
InnerClient { stream: None }
pub fn new() -> Client<S> {
Client { stream: None }
}
}
impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.command(QuitCommand);
@@ -165,9 +166,9 @@ impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
}
/// Checks if the server is connected using the NOOP SMTP command
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
#[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))]
pub fn is_connected(&mut self) -> bool {
self.stream.is_some() && self.command(NoopCommand).is_ok()
self.command(NoopCommand).is_ok()
}
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
@@ -193,11 +194,10 @@ 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<T: Read>(&mut self, mut message: Box<T>) -> SmtpResult {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new();
let mut message_reader = BufReader::new(message);
let mut message_reader = BufReader::new(message.as_mut());
loop {
out_buf.clear();
@@ -218,7 +218,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
self.write(out_buf.as_slice())?;
}
self.write(b"\r\n.\r\n")?;
self.write(MESSAGE_ENDING.as_bytes())?;
self.read_response()
}
@@ -254,13 +254,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
break;
}
// TODO read more than one line
let read_count = self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
// EOF is reached
if read_count == 0 {
break;
}
self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
response = raw_response.parse::<Response>();
}

View File

@@ -24,8 +24,8 @@ impl ClientTlsParameters {
}
/// Accepted protocols by default.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv12];
/// This removes TLS 1.0 compared to tls-native defaults.
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv11, Protocol::Tlsv12];
#[derive(Debug)]
/// Represents the different types of underlying network streams
@@ -117,7 +117,7 @@ impl Connector for NetworkStream {
}
}
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
*self = match *self {
NetworkStream::Tcp(ref mut stream) => match tls_parameters
@@ -134,7 +134,7 @@ impl Connector for NetworkStream {
Ok(())
}
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
fn is_encrypted(&self) -> bool {
match *self {
NetworkStream::Tcp(_) => false,

View File

@@ -1,15 +1,14 @@
#![cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
//! SMTP commands
use EmailAddress;
use base64;
use smtp::CRLF;
use smtp::authentication::{Credentials, Mechanism};
use smtp::error::Error;
use smtp::extension::ClientId;
use smtp::extension::{MailParameter, RcptParameter};
use smtp::extension::ClientId;
use smtp::response::Response;
use std::fmt::{self, Display, Formatter};
use EmailAddress;
/// EHLO command
#[derive(PartialEq, Clone, Debug)]
@@ -20,7 +19,8 @@ pub struct EhloCommand {
impl Display for EhloCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EHLO {}\r\n", self.client_id)
write!(f, "EHLO {}", self.client_id)?;
f.write_str(CRLF)
}
}
@@ -38,7 +38,8 @@ pub struct StarttlsCommand;
impl Display for StarttlsCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("STARTTLS\r\n")
f.write_str("STARTTLS")?;
f.write_str(CRLF)
}
}
@@ -55,12 +56,15 @@ impl Display for MailCommand {
write!(
f,
"MAIL FROM:<{}>",
self.sender.as_ref().map(|x| x.as_ref()).unwrap_or("")
match self.sender {
Some(ref address) => address.to_string(),
None => "".to_string(),
}
)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str("\r\n")
f.write_str(CRLF)
}
}
@@ -85,7 +89,7 @@ impl Display for RcptCommand {
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str("\r\n")
f.write_str(CRLF)
}
}
@@ -106,7 +110,8 @@ pub struct DataCommand;
impl Display for DataCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("DATA\r\n")
f.write_str("DATA")?;
f.write_str(CRLF)
}
}
@@ -117,7 +122,8 @@ pub struct QuitCommand;
impl Display for QuitCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("QUIT\r\n")
f.write_str("QUIT")?;
f.write_str(CRLF)
}
}
@@ -128,7 +134,8 @@ pub struct NoopCommand;
impl Display for NoopCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NOOP\r\n")
f.write_str("NOOP")?;
f.write_str(CRLF)
}
}
@@ -145,7 +152,7 @@ impl Display for HelpCommand {
if self.argument.is_some() {
write!(f, " {}", self.argument.as_ref().unwrap())?;
}
f.write_str("\r\n")
f.write_str(CRLF)
}
}
@@ -165,8 +172,8 @@ pub struct VrfyCommand {
impl Display for VrfyCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
write!(f, "VRFY {}\r\n", self.argument)
write!(f, "VRFY {}", self.argument)?;
f.write_str(CRLF)
}
}
@@ -186,7 +193,8 @@ pub struct ExpnCommand {
impl Display for ExpnCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EXPN {}\r\n", self.argument)
write!(f, "EXPN {}", self.argument)?;
f.write_str(CRLF)
}
}
@@ -204,7 +212,8 @@ pub struct RsetCommand;
impl Display for RsetCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("RSET\r\n")
f.write_str("RSET")?;
f.write_str(CRLF)
}
}
@@ -220,20 +229,24 @@ pub struct AuthCommand {
impl Display for AuthCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let encoded_response = self
.response
.as_ref()
.map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
let encoded_response = if self.response.is_some() {
Some(base64::encode_config(
self.response.as_ref().unwrap().as_bytes(),
base64::STANDARD,
))
} else {
None
};
if self.mechanism.supports_initial_response() {
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap(),)?;
} else {
match encoded_response {
Some(response) => f.write_str(&response)?,
None => write!(f, "AUTH {}", self.mechanism)?,
}
}
f.write_str("\r\n")
f.write_str(CRLF)
}
}
@@ -268,12 +281,21 @@ impl AuthCommand {
return Err(Error::ResponseParsing("Expecting a challenge"));
}
let encoded_challenge = response
.first_word()
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
let encoded_challenge = match response.first_word() {
Some(challenge) => challenge.to_string(),
None => return Err(Error::ResponseParsing("Could not read auth challenge")),
};
debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
let decoded_challenge = match base64::decode(&encoded_challenge) {
Ok(challenge) => match String::from_utf8(challenge) {
Ok(value) => value,
Err(error) => return Err(Error::Utf8Parsing(error)),
},
Err(error) => return Err(Error::ChallengeParsing(error)),
};
debug!("auth decoded challenge: {}", decoded_challenge);
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
@@ -291,6 +313,8 @@ impl AuthCommand {
mod test {
use super::*;
use smtp::extension::MailBodyParameter;
#[cfg(feature = "crammd5-auth")]
use smtp::response::{Category, Code, Detail, Severity};
#[test]
fn test_display() {
@@ -367,6 +391,18 @@ mod test {
),
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
);
#[cfg(feature = "crammd5-auth")]
assert_eq!(
format!(
"{}",
AuthCommand::new(
Mechanism::CramMd5,
credentials.clone(),
Some("test".to_string()),
).unwrap()
),
"dXNlciAzMTYxY2NmZDdmMjNlMzJiYmMzZTQ4NjdmYzk0YjE4Nw==\r\n"
);
assert_eq!(
format!(
"{}",
@@ -374,5 +410,24 @@ mod test {
),
"AUTH LOGIN\r\n"
);
#[cfg(feature = "crammd5-auth")]
assert_eq!(
format!(
"{}",
AuthCommand::new_from_response(
Mechanism::CramMd5,
credentials.clone(),
&Response::new(
Code::new(
Severity::PositiveIntermediate,
Category::Unspecified3,
Detail::Four,
),
vec!["dGVzdAo=".to_string()],
),
).unwrap()
),
"dXNlciA1NTIzNThiMzExOWFjOWNkYzM2YWRiN2MxNWRmMWJkNw==\r\n"
);
}
}

View File

@@ -47,7 +47,7 @@ impl Display for Error {
}
impl StdError for Error {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
fn description(&self) -> &str {
match *self {
// Try to display the first line of the server's response that usually
@@ -77,6 +77,7 @@ impl StdError for Error {
Utf8Parsing(ref err) => Some(&*err),
Io(ref err) => Some(&*err),
Tls(ref err) => Some(&*err),
Parsing(_) => None,
_ => None,
}
}
@@ -100,18 +101,6 @@ impl From<nom::ErrorKind> for Error {
}
}
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Error {
ChallengeParsing(err)
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8Parsing(err)
}
}
impl From<Response> for Error {
fn from(response: Response) -> Error {
match response.code.severity {

View File

@@ -10,8 +10,8 @@ use std::fmt::{self, Display, Formatter};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::result::Result;
/// Default client id
pub const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
/// Default ehlo clinet id
pub const DEFAULT_EHLO_HOSTNAME: &str = "localhost";
/// Client identifier, the parameter to `EHLO`
#[derive(PartialEq, Eq, Clone, Debug)]
@@ -44,7 +44,10 @@ impl ClientId {
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
/// found
pub fn hostname() -> ClientId {
ClientId::Domain(get_hostname().unwrap_or_else(|| DEFAULT_DOMAIN_CLIENT_ID.to_string()))
ClientId::Domain(match get_hostname() {
Some(name) => name,
None => DEFAULT_EHLO_HOSTNAME.to_string(),
})
}
}
@@ -134,22 +137,21 @@ impl ServerInfo {
"STARTTLS" => {
features.insert(Extension::StartTls);
}
"AUTH" => {
for &mechanism in &split[1..] {
match mechanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mechanism::Plain));
}
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
"XOAUTH2" => {
features.insert(Extension::Authentication(Mechanism::Xoauth2));
}
_ => (),
"AUTH" => for &mechanism in &split[1..] {
match mechanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mechanism::Plain));
}
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
#[cfg(feature = "crammd5-auth")]
"CRAM-MD5" => {
features.insert(Extension::Authentication(Mechanism::CramMd5));
}
_ => (),
}
}
},
_ => (),
};
}
@@ -355,6 +357,8 @@ mod test {
assert!(server_info.supports_feature(Extension::EightBitMime));
assert!(!server_info.supports_feature(Extension::StartTls));
#[cfg(feature = "crammd5-auth")]
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
let response2 = Response::new(
Code::new(
@@ -364,7 +368,7 @@ mod test {
),
vec![
"me".to_string(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
@@ -373,7 +377,8 @@ mod test {
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
#[cfg(feature = "crammd5-auth")]
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5),));
let server_info2 = ServerInfo {
name: "me".to_string(),
@@ -384,6 +389,8 @@ mod test {
assert!(server_info2.supports_feature(Extension::EightBitMime));
assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
#[cfg(feature = "crammd5-auth")]
assert!(server_info2.supports_auth_mechanism(Mechanism::CramMd5));
assert!(!server_info2.supports_feature(Extension::StartTls));
}
}

View File

@@ -8,33 +8,33 @@
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and
//! CRAM-MD5 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
use EmailTransport;
use SendableEmail;
use native_tls::TlsConnector;
use smtp::authentication::{
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
};
use smtp::authentication::{Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS,
DEFAULT_UNENCRYPTED_MECHANISMS};
use smtp::client::Client;
use smtp::client::net::ClientTlsParameters;
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
use smtp::client::InnerClient;
use smtp::commands::*;
use smtp::error::{Error, SmtpResult};
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
use std::io::Read;
use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
use {SendableEmail, Transport};
pub mod authentication;
pub mod client;
pub mod commands;
pub mod error;
pub mod extension;
#[cfg(feature = "connection-pool")]
pub mod r2d2;
pub mod commands;
pub mod authentication;
pub mod response;
pub mod client;
pub mod error;
pub mod util;
// Registered port numbers:
@@ -43,22 +43,39 @@ pub mod util;
/// Default smtp port
pub const SMTP_PORT: u16 = 25;
/// Default submission port
pub const SUBMISSION_PORT: u16 = 587;
/// Default submission over TLS port
pub const SUBMISSIONS_PORT: u16 = 465;
// Useful strings and characters
/// The word separator for SMTP transactions
pub const SP: &str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub const CRLF: &str = "\r\n";
/// Colon
pub const COLON: &str = ":";
/// The ending of message content
pub const MESSAGE_ENDING: &str = "\r\n.\r\n";
/// NUL unicode character
pub const NUL: &str = "\0";
/// How to apply TLS to a client connection
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub enum ClientSecurity {
/// Insecure connection only (for testing purposes)
/// Insecure connection
None,
/// Start with insecure connection and use `STARTTLS` when available
/// Use `STARTTLS` when available
Opportunistic(ClientTlsParameters),
/// Start with insecure connection and require `STARTTLS`
/// Always use `STARTTLS`
Required(ClientTlsParameters),
/// Use TLS wrapped connection
/// Use TLS wrapped connection without negotiation
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
Wrapper(ClientTlsParameters),
}
@@ -76,8 +93,7 @@ pub enum ConnectionReuseParameters {
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpClient {
pub struct SmtpTransportBuilder {
/// Enable connection reuse
connection_reuse: ConnectionReuseParameters,
/// Name sent during EHLO
@@ -98,7 +114,7 @@ pub struct SmtpClient {
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpClient {
impl SmtpTransportBuilder {
/// Creates a new SMTP client
///
/// Defaults are:
@@ -107,13 +123,14 @@ impl SmtpClient {
/// * No authentication
/// * No SMTPUTF8 support
/// * A 60 seconds timeout for smtp commands
///
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
pub fn new<A: ToSocketAddrs>(
addr: A,
security: ClientSecurity,
) -> Result<SmtpTransportBuilder, Error> {
let mut addresses = addr.to_socket_addrs()?;
match addresses.next() {
Some(addr) => Ok(SmtpClient {
Some(addr) => Ok(SmtpTransportBuilder {
server_addr: addr,
security,
smtp_utf8: false,
@@ -127,59 +144,41 @@ impl SmtpClient {
}
}
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new(
(domain, SUBMISSIONS_PORT),
ClientSecurity::Wrapper(tls_parameters),
)
}
/// Creates a new local SMTP client to port 25
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
}
/// Enable SMTPUTF8 if the server supports it
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder {
self.smtp_utf8 = enabled;
self
}
/// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
pub fn hello_name(mut self, name: ClientId) -> SmtpTransportBuilder {
self.hello_name = name;
self
}
/// Enable connection reuse
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
pub fn connection_reuse(
mut self,
parameters: ConnectionReuseParameters,
) -> SmtpTransportBuilder {
self.connection_reuse = parameters;
self
}
/// Set the client credentials
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpTransportBuilder {
self.credentials = Some(credentials.into());
self
}
/// Set the authentication mechanism to use
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpTransportBuilder {
self.authentication_mechanism = Some(mechanism);
self
}
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpTransportBuilder {
self.timeout = timeout;
self
}
@@ -187,7 +186,7 @@ impl SmtpClient {
/// Build the SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn transport(self) -> SmtpTransport {
pub fn build(self) -> SmtpTransport {
SmtpTransport::new(self)
}
}
@@ -210,9 +209,9 @@ pub struct SmtpTransport {
/// SmtpTransport variable states
state: State,
/// Information about the client
client_info: SmtpClient,
client_info: SmtpTransportBuilder,
/// Low level client
client: InnerClient,
client: Client,
}
macro_rules! try_smtp (
@@ -231,11 +230,40 @@ macro_rules! try_smtp (
);
impl<'a> SmtpTransport {
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submission port, using the provided domain
/// to validate TLS certificates.
pub fn simple_builder(domain: &str) -> Result<SmtpTransportBuilder, Error> {
let mut tls_builder = TlsConnector::builder()?;
tls_builder.supported_protocols(DEFAULT_TLS_PROTOCOLS)?;
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpTransportBuilder::new(
(domain, SUBMISSION_PORT),
ClientSecurity::Required(tls_parameters),
)
}
/// Creates a new configurable builder
pub fn builder<A: ToSocketAddrs>(
addr: A,
security: ClientSecurity,
) -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(addr, security)
}
/// Creates a new local SMTP client to port 25
pub fn builder_unencrypted_localhost() -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(("localhost", SMTP_PORT), ClientSecurity::None)
}
/// Creates a new SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpClient) -> SmtpTransport {
let client = InnerClient::new();
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
let client = Client::new();
SmtpTransport {
client,
@@ -248,99 +276,6 @@ impl<'a> SmtpTransport {
}
}
fn connect(&mut self) -> Result<(), Error> {
// Check if the connection is still available
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
self.close();
}
if self.state.connection_reuse_count > 0 {
info!(
"connection already established to {}",
self.client_info.server_addr
);
return Ok(());
}
self.client.connect(
&self.client_info.server_addr,
match self.client_info.security {
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
},
)?;
self.client.set_timeout(self.client_info.timeout)?;
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
self.ehlo()?;
match (
&self.client_info.security.clone(),
self.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::StartTls),
) {
(&ClientSecurity::Required(_), false) => {
return Err(From::from("Could not encrypt connection, aborting"));
}
(&ClientSecurity::Opportunistic(_), false) => (),
(&ClientSecurity::None, _) => (),
(&ClientSecurity::Wrapper(_), _) => (),
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
| (&ClientSecurity::Required(ref tls_parameters), true) => {
try_smtp!(self.client.command(StarttlsCommand), self);
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
debug!("connection encrypted");
// Send EHLO again
self.ehlo()?;
}
}
if self.client_info.credentials.is_some() {
let mut found = false;
// Compute accepted mechanism
let accepted_mechanisms = match self.client_info.authentication_mechanism {
Some(mechanism) => vec![mechanism],
None => {
if self.client.is_encrypted() {
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
} else {
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
}
}
};
for mechanism in accepted_mechanisms {
if self
.server_info
.as_ref()
.unwrap()
.supports_auth_mechanism(mechanism)
{
found = true;
try_smtp!(
self.client
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
self
);
break;
}
}
if !found {
info!("No supported authentication mechanisms available");
}
}
Ok(())
}
/// Gets the EHLO response and updates server information
fn ehlo(&mut self) -> SmtpResult {
// Extended Hello
@@ -371,26 +306,101 @@ impl<'a> SmtpTransport {
}
}
impl<'a> Transport<'a> for SmtpTransport {
type Result = SmtpResult;
impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
/// Sends an email
#[cfg_attr(
feature = "cargo-clippy",
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
)]
fn send(&mut self, email: SendableEmail) -> SmtpResult {
let message_id = email.message_id().to_string();
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms, cyclomatic_complexity))]
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SmtpResult {
// Extract email information
let message_id = email.message_id();
let envelope = email.envelope();
if !self.client.is_connected() {
self.connect()?;
// Check if the connection is still available
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
self.close();
}
if self.state.connection_reuse_count == 0 {
self.client.connect(
&self.client_info.server_addr,
match self.client_info.security {
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
},
)?;
self.client.set_timeout(self.client_info.timeout)?;
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
self.ehlo()?;
match (
&self.client_info.security.clone(),
self.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::StartTls),
) {
(&ClientSecurity::Required(_), false) => {
return Err(From::from("Could not encrypt connection, aborting"))
}
(&ClientSecurity::Opportunistic(_), false) => (),
(&ClientSecurity::None, _) => (),
(&ClientSecurity::Wrapper(_), _) => (),
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
| (&ClientSecurity::Required(ref tls_parameters), true) => {
try_smtp!(self.client.command(StarttlsCommand), self);
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
debug!("connection encrypted");
// Send EHLO again
self.ehlo()?;
}
}
if self.client_info.credentials.is_some() {
let mut found = false;
// Compute accepted mechanism
let accepted_mechanisms = match self.client_info.authentication_mechanism {
Some(mechanism) => vec![mechanism],
None => {
if self.client.is_encrypted() {
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
} else {
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
}
}
};
for mechanism in accepted_mechanisms {
if self.server_info
.as_ref()
.unwrap()
.supports_auth_mechanism(mechanism)
{
found = true;
try_smtp!(
self.client
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
self
);
break;
}
}
if !found {
info!("No supported authentication mechanisms available");
}
}
}
// Mail
let mut mail_options = vec![];
if self
.server_info
if self.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::EightBitMime)
@@ -398,21 +408,17 @@ impl<'a> Transport<'a> for SmtpTransport {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
if self
.server_info
if self.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::SmtpUtfEight)
&& self.client_info.smtp_utf8
.supports_feature(Extension::SmtpUtfEight) && self.client_info.smtp_utf8
{
mail_options.push(MailParameter::SmtpUtfEight);
}
try_smtp!(
self.client.command(MailCommand::new(
email.envelope().from().cloned(),
mail_options,
)),
self.client
.command(MailCommand::new(envelope.from().cloned(), mail_options,)),
self
);
@@ -420,17 +426,17 @@ impl<'a> Transport<'a> for SmtpTransport {
info!(
"{}: from=<{}>",
message_id,
match email.envelope().from() {
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![])),
.command(RcptCommand::new(to_address.clone(), vec![]),),
self
);
// Log the rcpt command
@@ -441,7 +447,7 @@ impl<'a> Transport<'a> for SmtpTransport {
try_smtp!(self.client.command(DataCommand), self);
// Message content
let result = self.client.message(Box::new(email.message()));
let result = self.client.message(email.message());
if result.is_ok() {
// Increment the connection reuse counter

View File

@@ -1,38 +0,0 @@
use r2d2::ManageConnection;
use smtp::error::Error;
use smtp::{ConnectionReuseParameters, SmtpClient, SmtpTransport};
pub struct SmtpConnectionManager {
transport_builder: SmtpClient,
}
impl SmtpConnectionManager {
pub fn new(transport_builder: SmtpClient) -> Result<SmtpConnectionManager, Error> {
Ok(SmtpConnectionManager {
transport_builder: transport_builder
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited),
})
}
}
impl ManageConnection for SmtpConnectionManager {
type Connection = SmtpTransport;
type Error = Error;
fn connect(&self) -> Result<Self::Connection, Error> {
let mut transport = SmtpTransport::new(self.transport_builder.clone());
transport.connect()?;
Ok(transport)
}
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
if conn.client.is_connected() {
return Ok(());
}
Err(Error::Client("is not connected anymore"))
}
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
conn.state.panic
}
}

View File

@@ -4,7 +4,7 @@
use nom::{crlf, ErrorKind as NomErrorKind};
use std::fmt::{Display, Formatter, Result};
use std::result;
use std::str::{from_utf8, FromStr};
use std::str::{FromStr, from_utf8};
/// First digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
@@ -331,19 +331,20 @@ mod test {
#[test]
fn test_response_is_positive() {
assert!(Response::new(
Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.is_positive());
assert!(
Response::new(
Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).is_positive()
);
assert!(!Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
@@ -355,25 +356,25 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.is_positive());
).is_positive());
}
#[test]
fn test_response_has_code() {
assert!(Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.has_code(451));
assert!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).has_code(451)
);
assert!(!Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
@@ -385,8 +386,7 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.has_code(251));
).has_code(251));
}
#[test]
@@ -403,8 +403,7 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_word(),
).first_word(),
Some("me")
);
assert_eq!(
@@ -419,8 +418,7 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_word(),
).first_word(),
Some("me")
);
assert_eq!(
@@ -431,8 +429,7 @@ mod test {
detail: Detail::One,
},
vec![],
)
.first_word(),
).first_word(),
None
);
assert_eq!(
@@ -443,8 +440,7 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_word(),
).first_word(),
None
);
assert_eq!(
@@ -455,8 +451,7 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_word(),
).first_word(),
None
);
assert_eq!(
@@ -467,8 +462,7 @@ mod test {
detail: Detail::One,
},
vec!["".to_string()],
)
.first_word(),
).first_word(),
None
);
}
@@ -487,8 +481,7 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_line(),
).first_line(),
Some("me")
);
assert_eq!(
@@ -503,8 +496,7 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_line(),
).first_line(),
Some("me mo")
);
assert_eq!(
@@ -515,8 +507,7 @@ mod test {
detail: Detail::One,
},
vec![],
)
.first_line(),
).first_line(),
None
);
assert_eq!(
@@ -527,8 +518,7 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_line(),
).first_line(),
Some(" ")
);
assert_eq!(
@@ -539,8 +529,7 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_line(),
).first_line(),
Some(" ")
);
assert_eq!(
@@ -551,8 +540,7 @@ mod test {
detail: Detail::One,
},
vec!["".to_string()],
)
.first_line(),
).first_line(),
Some("")
);
}

View File

@@ -2,42 +2,42 @@
//! testing purposes.
//!
use EmailTransport;
use SendableEmail;
use Transport;
use std::io::Read;
/// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)]
pub struct StubTransport {
pub struct StubEmailTransport {
response: StubResult,
}
impl StubTransport {
impl StubEmailTransport {
/// Creates a new transport that always returns the given response
pub fn new(response: StubResult) -> StubTransport {
StubTransport { response }
pub fn new(response: StubResult) -> StubEmailTransport {
StubEmailTransport { response }
}
/// Creates a new transport that always returns a success response
pub fn new_positive() -> StubTransport {
StubTransport { response: Ok(()) }
pub fn new_positive() -> StubEmailTransport {
StubEmailTransport { response: Ok(()) }
}
}
/// SMTP result type
pub type StubResult = Result<(), ()>;
impl<'a> Transport<'a> for StubTransport {
type Result = StubResult;
fn send(&mut self, email: SendableEmail) -> StubResult {
impl<'a, T: Read + 'a> EmailTransport<'a, T, StubResult> for StubEmailTransport {
fn send<U: SendableEmail<'a, T>>(&mut self, email: &'a U) -> StubResult {
let envelope = email.envelope();
info!(
"{}: from=<{}> to=<{:?}>",
email.message_id(),
match email.envelope().from() {
match envelope.from() {
Some(address) => address.to_string(),
None => "".to_string(),
},
email.envelope().to()
envelope.to()
);
self.response
}

View File

@@ -1,74 +0,0 @@
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
mod test {
extern crate lettre;
extern crate r2d2;
use self::lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient};
use self::lettre::{SmtpConnectionManager, Transport};
use self::r2d2::Pool;
use std::sync::mpsc;
use std::thread;
fn email(message: &str) -> SendableEmail {
SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
message.to_string().into_bytes(),
)
}
#[test]
fn send_one() {
let client = SmtpClient::new("localhost:2525", ClientSecurity::None).unwrap();
let manager = SmtpConnectionManager::new(client).unwrap();
let pool = Pool::builder().max_size(1).build(manager).unwrap();
let mut mailer = pool.get().unwrap();
let result = (*mailer).send(email("send one"));
assert!(result.is_ok());
}
#[test]
fn send_from_thread() {
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap();
let manager = SmtpConnectionManager::new(client).unwrap();
let pool = Pool::builder().max_size(2).build(manager).unwrap();
let (s1, r1) = mpsc::channel();
let (s2, r2) = mpsc::channel();
let pool1 = pool.clone();
let t1 = thread::spawn(move || {
let mut conn = pool1.get().unwrap();
s1.send(()).unwrap();
r2.recv().unwrap();
(*conn)
.send(email("send from thread 1"))
.expect("Send failed from thread 1");
drop(conn);
});
let pool2 = pool.clone();
let t2 = thread::spawn(move || {
let mut conn = pool2.get().unwrap();
s2.send(()).unwrap();
r1.recv().unwrap();
(*conn)
.send(email("send from thread 2"))
.expect("Send failed from thread 2");
drop(conn);
});
t1.join().unwrap();
t2.join().unwrap();
let mut mailer = pool.get().unwrap();
(*mailer)
.send(email("send from main thread"))
.expect("Send failed from main thread");
}
}

View File

@@ -1,12 +1,11 @@
extern crate glob;
use self::glob::glob;
use std::env;
use std::env::consts::EXE_EXTENSION;
use std::env;
use std::path::Path;
use std::process::Command;
/*
#[test]
fn test_readme() {
let readme = Path::new(file!())
@@ -20,7 +19,6 @@ fn test_readme() {
skeptic_test(&readme);
}
*/
#[test]
fn book_test() {
@@ -52,8 +50,7 @@ fn skeptic_test(path: &Path) {
.arg(&depdir)
.arg(path);
let result = cmd
.spawn()
let result = cmd.spawn()
.expect("Failed to spawn process")
.wait()
.expect("Failed to run process");

View File

@@ -4,38 +4,34 @@ extern crate lettre;
#[cfg(feature = "file-transport")]
mod test {
use lettre::file::FileTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
use lettre::{EmailTransport, SendableEmail, SimpleSendableEmail};
use lettre::file::FileEmailTransport;
use std::env::temp_dir;
use std::fs::remove_file;
use std::fs::File;
use std::fs::remove_file;
use std::io::Read;
#[test]
fn file_transport() {
let mut sender = FileTransport::new(temp_dir());
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let message_id = email.message_id().to_string();
let result = sender.send(email);
let mut sender = FileEmailTransport::new(temp_dir());
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"file_id".to_string(),
"Hello file".to_string(),
).unwrap();
let result = sender.send(&email);
assert!(result.is_ok());
let file = format!("{}/{}.json", temp_dir().to_str().unwrap(), message_id);
let message_id = email.message_id();
let file = format!("{}/{}.txt", temp_dir().to_str().unwrap(), message_id);
let mut f = File::open(file.clone()).unwrap();
let mut buffer = String::new();
let _ = f.read_to_string(&mut buffer);
assert_eq!(
buffer,
"{\"envelope\":{\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\"message_id\":\"id\",\"message\":[72,101,108,108,111,32,195,159,226,152,186,32,101,120,97,109,112,108,101]}"
"{\"envelope\":{\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\"message_id\":\"file_id\",\"message\":[72,101,108,108,111,32,102,105,108,101]}"
);
remove_file(file).unwrap();

View File

@@ -3,23 +3,21 @@ extern crate lettre;
#[cfg(test)]
#[cfg(feature = "sendmail-transport")]
mod test {
use lettre::{EmailTransport, SimpleSendableEmail};
use lettre::sendmail::SendmailTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
#[test]
fn sendmail_transport_simple() {
let mut sender = SendmailTransport::new();
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"sendmail_id".to_string(),
"Hello sendmail".to_string(),
).unwrap();
let result = sender.send(email);
let result = sender.send(&email);
println!("{:?}", result);
assert!(result.is_ok());
}

View File

@@ -3,25 +3,22 @@ extern crate lettre;
#[cfg(test)]
#[cfg(feature = "smtp-transport")]
mod test {
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
use lettre::{ClientSecurity, EmailTransport, SimpleSendableEmail, SmtpTransport};
#[test]
fn smtp_transport_simple() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.transport()
.send(email)
.unwrap();
.build();
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"smtp_id".to_string(),
"Hello smtp".to_string(),
).unwrap();
sender.send(&email).unwrap();
}
}

View File

@@ -1,31 +1,19 @@
extern crate lettre;
use lettre::stub::StubTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
use lettre::{EmailTransport, SimpleSendableEmail};
use lettre::stub::StubEmailTransport;
#[test]
fn stub_transport() {
let mut sender_ok = StubTransport::new_positive();
let mut sender_ko = StubTransport::new(Err(()));
let email_ok = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let email_ko = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let mut sender_ok = StubEmailTransport::new_positive();
let mut sender_ko = StubEmailTransport::new(Err(()));
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"stub_id".to_string(),
"Hello stub".to_string(),
).unwrap();
sender_ok.send(email_ok).unwrap();
sender_ko.send(email_ko).unwrap_err();
sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err();
}

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre_email"
version = "0.9.2" # remember to update html_root_url
version = "0.8.2" # remember to update html_root_url
description = "Email builder"
readme = "README.md"
homepage = "http://lettre.at"
@@ -19,13 +19,13 @@ is-it-maintained-issue-resolution = { repository = "lettre/lettre_email" }
is-it-maintained-open-issues = { repository = "lettre/lettre_email" }
[dev-dependencies]
lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
glob = "0.3"
lettre = { version = "^0.8", path = "../lettre", features = ["smtp-transport"] }
glob = "0.2"
[dependencies]
email = "^0.0.20"
email = "^0.0"
mime = "^0.3"
time = "^0.1"
uuid = { version = "^0.7", features = ["v4"] }
lettre = { version = "^0.9", path = "../lettre", default-features = false }
base64 = "^0.10"
uuid = { version = "^0.6", features = ["v4"] }
lettre = { version = "^0.8", path = "../lettre", default-features = false }
base64 = "^0.9"

View File

@@ -2,27 +2,28 @@ extern crate lettre;
extern crate lettre_email;
extern crate mime;
use lettre::{SmtpClient, Transport};
use lettre_email::Email;
use lettre::{EmailTransport, SmtpTransport};
use lettre_email::EmailBuilder;
use std::path::Path;
fn main() {
let email = Email::builder()
let email = EmailBuilder::new()
// Addresses can be specified by the tuple (email, alias)
.to(("user@example.org", "Firstname Lastname"))
// ... or by an address only
.from("user@example.com")
.subject("Hi, Hello world")
.text("Hello world.")
.attachment_from_file(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN)
.unwrap()
.attachment(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN).unwrap()
.build()
.unwrap();
// Open a local connection on port 25
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
let mut mailer = SmtpTransport::builder_unencrypted_localhost()
.unwrap()
.build();
// Send the email
let result = mailer.send(email.into());
let result = mailer.send(&email);
if result.is_ok() {
println!("Email sent");

View File

@@ -1,18 +1,17 @@
//! Error and result type for emails
use lettre;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
use self::Error::*;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
use lettre;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Envelope error
Envelope(lettre::error::Error),
Email(lettre::Error),
/// Unparseable filename for attachment
CannotParseFilename,
/// IO error
@@ -20,34 +19,29 @@ pub enum 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(),
})
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
fn description(&self) -> &str {
match *self {
Envelope(ref err) => Some(err),
Io(ref err) => Some(err),
_ => None,
Email(ref err) => err.description(),
CannotParseFilename => "the attachment filename could not be parsed",
Io(ref err) => err.description(),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
Io(err)
}
}
impl From<lettre::error::Error> for Error {
fn from(err: lettre::error::Error) -> Error {
Error::Envelope(err)
impl From<lettre::Error> for Error {
fn from(err: lettre::Error) -> Error {
Email(err)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +0,0 @@
extern crate lettre;
extern crate lettre_email;
use lettre::{EmailAddress, Envelope};
use lettre_email::EmailBuilder;
#[test]
fn build_with_envelope_test() {
let e = Envelope::new(
Some(EmailAddress::new("from@example.org".to_string()).unwrap()),
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
)
.unwrap();
let _email = EmailBuilder::new()
.envelope(e)
.subject("subject")
.text("message")
.build()
.unwrap();
}
#[test]
fn build_with_envelope_without_from_test() {
let e = Envelope::new(
None,
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
)
.unwrap();
let _email = EmailBuilder::new()
.envelope(e)
.subject("subject")
.text("message")
.build()
.unwrap_err();
}

View File

@@ -1,8 +1,8 @@
extern crate glob;
use self::glob::glob;
use std::env;
use std::env::consts::EXE_EXTENSION;
use std::env;
use std::path::Path;
use std::process::Command;
@@ -36,8 +36,7 @@ fn skeptic_test(path: &Path) {
.arg(&depdir)
.arg(path);
let result = cmd
.spawn()
let result = cmd.spawn()
.expect("Failed to spawn process")
.wait()
.expect("Failed to run process");

1
website/.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules
/_book

View File

@@ -1,16 +1,13 @@
all: depends _book
depends:
cargo install --force mdbook --vers "^0.2"
cargo install --force mdbook-linkcheck --vers "^0.2"
gitbook install
serve:
mdbook serve
gitbook serve
_book:
mdbook build
gitbook build
clean:
rm -rf _book/
.PHONY: _book

10
website/book.json Normal file
View File

@@ -0,0 +1,10 @@
{
"root": "./content",
"plugins": [ "-sharing", "edit-link" ],
"pluginsConfig": {
"edit-link": {
"base": "https://github.com/lettre/lettre/edit/master/website/src",
"label": "Edit"
}
}
}

View File

@@ -1,11 +0,0 @@
[book]
title = "Lettre"
author = "Alexis Mousset"
description = "The user documentation of the Lettre crate."
[build]
build-dir = "_book"
[output.html]
[output.linkcheck]

View File

@@ -10,10 +10,10 @@ Lettre is an email library that allows creating and sending messages. It provide
The `lettre_email` crate allows you to compose messages, and the `lettre`
provide transports to send them.
Lettre requires Rust 1.32 or newer. Add the following to your `Cargo.toml`:
Lettre requires Rust 1.20 or newer. Add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.9"
lettre_email = "0.9"
lettre = "0.8"
lettre_email = "0.8"
```

View File

@@ -10,17 +10,17 @@ An email is built using an `EmailBuilder`. The simplest email could be:
```rust
extern crate lettre_email;
use lettre_email::Email;
use lettre_email::EmailBuilder;
fn main() {
// Create an email
let email = Email::builder()
let email = EmailBuilder::new()
// Addresses can be specified by the tuple (email, alias)
.to(("user@example.org", "Firstname Lastname"))
// ... or by an address only
.from("user@example.com")
.subject("Hi, Hello world")
.alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.")
.text("Hello world.")
.build();
assert!(email.is_ok());
@@ -34,3 +34,32 @@ then generate an `Email` that can be sent.
The `text()` method will create a plain text email, while the `html()` method will create an
HTML email. You can use the `alternative()` method to provide both versions, using plain text
as fallback for the HTML version.
#### Complete example
Below is a more complete example, not using method chaining:
```rust
extern crate lettre_email;
use lettre_email::EmailBuilder;
fn main() {
let mut builder = EmailBuilder::new();
builder.add_to(("user@example.org", "Alias name"));
builder.add_cc(("user@example.net", "Alias name"));
builder.add_from("no-reply@example.com");
builder.add_from("no-reply@example.eu");
builder.set_sender("no-reply@example.com");
builder.set_subject("Hello world");
builder.set_alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.");
builder.add_reply_to("contact@example.com");
builder.add_header(("X-Custom-Header", "my header"));
let email = builder.build();
assert!(email.is_ok());
}
```
See the `EmailBuilder` documentation for a complete list of methods.

View File

@@ -9,22 +9,20 @@ extern crate lettre;
use std::env::temp_dir;
use lettre::file::FileTransport;
use lettre::{Transport, Envelope, EmailAddress, SendableEmail};
use lettre::file::FileEmailTransport;
use lettre::{SimpleSendableEmail, EmailTransport};
fn main() {
// Write to the local temp directory
let mut sender = FileTransport::new(temp_dir());
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"id".to_string(),
"Hello world".to_string().into_bytes(),
);
let mut sender = FileEmailTransport::new(temp_dir());
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
let result = sender.send(email);
let result = sender.send(&email);
assert!(result.is_ok());
}
```

View File

@@ -0,0 +1,23 @@
#### Sendmail Transport
The sendmail transport sends the email using the local sendmail command.
```rust,no_run
extern crate lettre;
use lettre::sendmail::SendmailTransport;
use lettre::{SimpleSendableEmail, EmailTransport};
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
let mut sender = SendmailTransport::new();
let result = sender.send(&email);
assert!(result.is_ok());
}
```

View File

@@ -1,4 +1,4 @@
#### SMTP Transport
SMTP Transport
This transport uses the SMTP protocol to send emails over the network (locally or remotely).
@@ -20,23 +20,21 @@ This is the most basic example of usage:
```rust,no_run
extern crate lettre;
use lettre::{SendableEmail, EmailAddress, Transport, Envelope, SmtpClient};
use lettre::{SimpleSendableEmail, EmailTransport, SmtpTransport};
fn main() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"id".to_string(),
"Hello world".to_string().into_bytes(),
);
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
// Open a local connection on port 25
let mut mailer =
SmtpClient::new_unencrypted_localhost().unwrap().transport();
SmtpTransport::builder_unencrypted_localhost().unwrap().build();
// Send the email
let result = mailer.send(email);
let result = mailer.send(&email);
assert!(result.is_ok());
}
@@ -48,31 +46,20 @@ fn main() {
extern crate lettre;
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::{SendableEmail, Envelope, EmailAddress, Transport, SmtpClient};
use lettre::{SimpleSendableEmail, EmailTransport, SmtpTransport};
use lettre::smtp::extension::ClientId;
use lettre::smtp::ConnectionReuseParameters;
fn main() {
let email_1 = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"id1".to_string(),
"Hello world".to_string().into_bytes(),
);
let email_2 = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"id2".to_string(),
"Hello world a second time".to_string().into_bytes(),
);
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
// Connect to a remote server on a custom port
let mut mailer = SmtpClient::new_simple("server.tld").unwrap()
let mut mailer = SmtpTransport::simple_builder("server.tld").unwrap()
// Set the name sent during EHLO/HELO, default is `localhost`
.hello_name(ClientId::Domain("my.hostname.tld".to_string()))
// Add credentials for authentication
@@ -82,13 +69,13 @@ fn main() {
// Configure expected authentication mechanism
.authentication_mechanism(Mechanism::Plain)
// Enable connection reuse
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited).build();
let result_1 = mailer.send(email_1);
let result_1 = mailer.send(&email);
assert!(result_1.is_ok());
// The second email will use the same connection
let result_2 = mailer.send(email_2);
let result_2 = mailer.send(&email);
assert!(result_2.is_ok());
// Explicitly close the SMTP transaction as we enabled connection reuse
@@ -103,46 +90,45 @@ extern crate native_tls;
extern crate lettre;
extern crate lettre_email;
use lettre::{
ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
SendableEmail, SmtpClient, Transport,
};
use native_tls::TlsConnector;
use native_tls::{Protocol};
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::{EmailTransport, SimpleSendableEmail, ClientTlsParameters, ClientSecurity};
use lettre::smtp::ConnectionReuseParameters;
use native_tls::{Protocol, TlsConnector};
use lettre::smtp::{SmtpTransportBuilder};
use lettre_email::EmailBuilder;
fn main() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"message_id".to_string(),
"Hello world".to_string().into_bytes(),
);
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
let mut tls_builder = TlsConnector::builder().unwrap();
tls_builder.supported_protocols(&[Protocol::Tlsv10]).unwrap();
let tls_parameters =
ClientTlsParameters::new(
"smtp.example.com".to_string(),
tls_builder.build().unwrap()
);
let mut mailer = SmtpClient::new(
let mut mailer = SmtpTransportBuilder::new(
("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters)
).unwrap()
)
.expect("Failed to create transport")
.authentication_mechanism(Mechanism::Login)
.credentials(Credentials::new(
"example_username".to_string(), "example_password".to_string()
))
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.transport();
let result = mailer.send(email);
.build();
let result = mailer.send(&email);
assert!(result.is_ok());
mailer.close();
}
```
@@ -157,13 +143,13 @@ extern crate lettre;
use lettre::EmailAddress;
use lettre::smtp::SMTP_PORT;
use lettre::smtp::client::InnerClient;
use lettre::smtp::client::Client;
use lettre::smtp::client::net::NetworkStream;
use lettre::smtp::extension::ClientId;
use lettre::smtp::commands::*;
fn main() {
let mut email_client: InnerClient<NetworkStream> = InnerClient::new();
let mut email_client: Client<NetworkStream> = Client::new();
let _ = email_client.connect(&("localhost", SMTP_PORT), None);
let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
let _ = email_client.command(

View File

@@ -0,0 +1,30 @@
#### Stub Transport
The stub transport only logs message envelope and drops the content. It can be useful for
testing purposes.
```rust
extern crate lettre;
use lettre::stub::StubEmailTransport;
use lettre::{SimpleSendableEmail, EmailTransport};
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
let mut sender = StubEmailTransport::new_positive();
let result = sender.send(&email);
assert!(result.is_ok());
}
```
Will log (when using a logger like `env_logger`):
```text
b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
```

View File

@@ -1,25 +0,0 @@
#### Sendmail Transport
The sendmail transport sends the email using the local sendmail command.
```rust,no_run
extern crate lettre;
use lettre::sendmail::SendmailTransport;
use lettre::{SendableEmail, Envelope, EmailAddress, Transport};
fn main() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"id".to_string(),
"Hello world".to_string().into_bytes(),
);
let mut sender = SendmailTransport::new();
let result = sender.send(email);
assert!(result.is_ok());
}
```

View File

@@ -1,32 +0,0 @@
#### Stub Transport
The stub transport only logs message envelope and drops the content. It can be useful for
testing purposes.
```rust
extern crate lettre;
use lettre::stub::StubTransport;
use lettre::{SendableEmail, Envelope, EmailAddress, Transport};
fn main() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"id".to_string(),
"Hello world".to_string().into_bytes(),
);
let mut sender = StubTransport::new_positive();
let result = sender.send(email);
assert!(result.is_ok());
}
```
Will log (when using a logger like `env_logger`):
```text
b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
```

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB