Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54032b5ce5 | ||
|
|
6a40f4a5fe | ||
|
|
a468cb3ab8 | ||
|
|
9b591ff932 | ||
|
|
eff4e1693f | ||
|
|
75ab05229a | ||
|
|
393ef8dcd1 | ||
|
|
ceb57edfdd | ||
|
|
cf8f934c56 | ||
|
|
df949f837e | ||
|
|
0a3d51dc25 | ||
|
|
4828cf4e92 | ||
|
|
c33de49fbb | ||
|
|
4f470a2c3f | ||
|
|
a0c8fb947c | ||
|
|
101189882a | ||
|
|
be50862d55 | ||
|
|
2d4320ea45 | ||
|
|
0444e7833b | ||
|
|
6997ab7ce4 | ||
|
|
139eb9e67e | ||
|
|
917d34210d | ||
|
|
ed2730a0a7 | ||
|
|
81d174d7ed | ||
|
|
bdb4f78bd3 | ||
|
|
4a76fbb46c | ||
|
|
52535c4554 | ||
|
|
5918803778 | ||
|
|
72f3cd8f12 | ||
|
|
78ba8007cd | ||
|
|
e202eafb7f | ||
|
|
adbd50a6ce | ||
|
|
058fa694f0 | ||
|
|
f64721702f | ||
|
|
a8d8e2ac00 | ||
|
|
434654e9af | ||
|
|
4a77f587c3 | ||
|
|
c988b1760a | ||
|
|
e08d4e3ee5 | ||
|
|
fc91bb6ee8 | ||
|
|
ee31bbe9e3 | ||
|
|
0b92881b48 | ||
|
|
8bb97e62ca | ||
|
|
8ba1c3a3f7 | ||
|
|
644b1e59b0 | ||
|
|
e225afbec2 | ||
|
|
22555f0620 | ||
|
|
c09e3ff8cd | ||
|
|
c10fe3db84 | ||
|
|
9d14630552 | ||
|
|
186ad29424 | ||
|
|
dce8d53310 | ||
|
|
c9c82495ce | ||
|
|
283d45824e | ||
|
|
0ee089fc37 | ||
|
|
7f545301e1 | ||
|
|
ce932c15d6 | ||
|
|
afc23de20f | ||
|
|
c52c28ab80 | ||
|
|
4f9067f258 | ||
|
|
964b9dc00b | ||
|
|
866c804ef3 | ||
|
|
a7d35325ed | ||
|
|
319be26031 | ||
|
|
3a0b6e1a31 | ||
|
|
ed7c16452c | ||
|
|
2e2f614517 | ||
|
|
bc09aa2185 | ||
|
|
bab4519baa | ||
|
|
1f1359502e | ||
|
|
5423f53bad | ||
|
|
4b48bdbd9a | ||
|
|
31442e96d0 | ||
|
|
70720d7cdd | ||
|
|
706ed8b4fd | ||
|
|
da63de72fc | ||
|
|
d71b560077 | ||
|
|
917ecbc477 | ||
|
|
4a61357205 | ||
|
|
e7e0f3485d | ||
|
|
33d20c61d1 | ||
|
|
1baf8a9516 | ||
|
|
5bb4f4f8e7 | ||
|
|
2e56bd6a82 | ||
|
|
3159981e4a | ||
|
|
a0c95f748e | ||
|
|
f949dd53ed | ||
|
|
d990ab4de3 | ||
|
|
c489a0bdc2 | ||
|
|
9dd08ad4c2 | ||
|
|
4313207896 | ||
|
|
944a236aa7 | ||
|
|
ddd80f5dcd | ||
|
|
530b595424 | ||
|
|
f985cf7559 | ||
|
|
7b6ac2e677 | ||
|
|
6797d3d3f5 | ||
|
|
bffe2978d2 | ||
|
|
4d86840bc9 | ||
|
|
a7e5493aad | ||
|
|
d6828f5150 | ||
|
|
0b850e3b2f | ||
|
|
57be14112a | ||
|
|
928fd413a4 | ||
|
|
2d196599c7 | ||
|
|
ad2fef9bbc | ||
|
|
6577aa17d9 | ||
|
|
34fc101f31 | ||
|
|
91f0cfa27c | ||
|
|
78bd429310 | ||
|
|
54e3dd3e41 | ||
|
|
36381eb345 | ||
|
|
8d92dbb0c2 | ||
|
|
c4b1100bdb | ||
|
|
b5e2c67dbd | ||
|
|
bf3bb78534 | ||
|
|
a914d9990c |
@@ -4,7 +4,7 @@ set -xe
|
||||
|
||||
cd website
|
||||
make clean && make
|
||||
echo "lettre.at" > _book/CNAME
|
||||
echo "lettre.at" > _book/html/CNAME
|
||||
sudo pip install ghp-import
|
||||
ghp-import -n _book
|
||||
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
||||
ghp-import -n _book/html
|
||||
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
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.
|
||||
17
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
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.
|
||||
@@ -3,7 +3,7 @@ rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
- 1.20.0
|
||||
- 1.32.0
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
@@ -19,13 +19,11 @@ 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
|
||||
- cargo test --verbose --all --all-features
|
||||
after_success:
|
||||
- ./.build-scripts/codecov.sh
|
||||
- '[ "$TRAVIS_BRANCH" = "v0.8.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'
|
||||
- '[ "$TRAVIS_RUST_VERSION" = "stable" ] && [ "$TRAVIS_BRANCH" = "v0.9.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'
|
||||
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -1,3 +1,69 @@
|
||||
<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)
|
||||
|
||||
#### 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))
|
||||
|
||||
<a name="v0.8.2"></a>
|
||||
### v0.8.2 (2018-05-03)
|
||||
|
||||
|
||||
@@ -34,19 +34,18 @@ Lettre provides the following features:
|
||||
|
||||
## Example
|
||||
|
||||
This library requires Rust 1.20 or newer.
|
||||
This library requires Rust 1.32 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.8"
|
||||
lettre_email = "0.8"
|
||||
lettre = "0.9"
|
||||
lettre_email = "0.9"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
extern crate mime;
|
||||
|
||||
use lettre::{EmailTransport, SmtpTransport};
|
||||
use lettre_email::EmailBuilder;
|
||||
@@ -60,7 +59,6 @@ fn main() {
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.text("Hello world.")
|
||||
.attachment(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN).unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
|
||||
name = "lettre"
|
||||
version = "0.8.2" # remember to update html_root_url
|
||||
version = "0.9.3" # remember to update html_root_url
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "http://lettre.at"
|
||||
@@ -20,31 +20,34 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
|
||||
[dependencies]
|
||||
log = "^0.4"
|
||||
nom = { version = "^3.2", optional = true }
|
||||
nom = { version = "^4.0", optional = true }
|
||||
bufstream = { version = "^0.1", optional = true }
|
||||
native-tls = { version = "^0.1", optional = true }
|
||||
base64 = { version = "^0.9", optional = true }
|
||||
hex = { version = "^0.3", optional = true }
|
||||
native-tls = { version = "^0.2", optional = true }
|
||||
base64 = { version = "^0.10", 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.5"
|
||||
glob = "0.2"
|
||||
env_logger = "^0.6"
|
||||
glob = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["file-transport", "crammd5-auth", "smtp-transport", "sendmail-transport"]
|
||||
default = ["file-transport", "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"]
|
||||
|
||||
@@ -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,13 +13,16 @@ fn bench_simple_send(b: &mut test::Bencher) {
|
||||
.unwrap()
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SimpleSendableEmail::new(
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
vec![EmailAddress::new("root@localhost".to_string())],
|
||||
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(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let result = sender.send(&email);
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
}
|
||||
@@ -31,13 +34,16 @@ fn bench_reuse_send(b: &mut test::Bencher) {
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SimpleSendableEmail::new(
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
vec![EmailAddress::new("root@localhost".to_string())],
|
||||
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(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let result = sender.send(&email);
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
sender.close()
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
extern crate env_logger;
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{EmailTransport, SimpleSendableEmail, SmtpTransport};
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let email = SimpleSendableEmail::new(
|
||||
"user@localhost".to_string(),
|
||||
&["root@localhost".to_string()],
|
||||
"my-message-id".to_string(),
|
||||
"Hello ß☺ example".to_string(),
|
||||
).unwrap();
|
||||
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(),
|
||||
);
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost()
|
||||
.unwrap()
|
||||
.build();
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(&email);
|
||||
let result = mailer.send(email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
|
||||
38
lettre/examples/smtp_gmail.rs
Normal file
38
lettre/examples/smtp_gmail.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
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());
|
||||
}
|
||||
35
lettre/src/error.rs
Normal file
35
lettre/src/error.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
/// 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>;
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
use self::Error::*;
|
||||
use serde_json;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::io;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
@@ -19,20 +21,16 @@ pub enum Error {
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
match *self {
|
||||
Client(err) => fmt.write_str(err),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
JsonSerialization(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Client(err) => err,
|
||||
Io(ref err) => err.description(),
|
||||
JsonSerialization(ref err) => err.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
JsonSerialization(ref err) => Some(&*err),
|
||||
@@ -43,19 +41,19 @@ impl StdError for Error {
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Io(err)
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Error {
|
||||
JsonSerialization(err)
|
||||
Error::JsonSerialization(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Client(string)
|
||||
Error::Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,52 +3,58 @@
|
||||
//! 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 FileEmailTransport {
|
||||
pub struct FileTransport {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileEmailTransport {
|
||||
impl FileTransport {
|
||||
/// Creates a new transport to the given directory
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileEmailTransport {
|
||||
let mut path_buf = PathBuf::new();
|
||||
path_buf.push(path);
|
||||
FileEmailTransport { path: path_buf }
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Read + 'a> EmailTransport<'a, T, FileResult> for FileEmailTransport {
|
||||
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> FileResult {
|
||||
#[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();
|
||||
|
||||
let mut file = self.path.clone();
|
||||
file.push(format!("{}.txt", email.message_id()));
|
||||
file.push(format!("{}.json", message_id));
|
||||
|
||||
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())?;
|
||||
let serialized = serde_json::to_string(&SerializableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: email.message_to_string()?.as_bytes().to_vec(),
|
||||
})?;
|
||||
|
||||
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +1,80 @@
|
||||
//! 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. To be sendable, the
|
||||
//! emails have to implement `SendableEmail`.
|
||||
//! This mailer contains the available transports for your emails.
|
||||
//!
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/lettre/0.8.2")]
|
||||
#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts,
|
||||
trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces,
|
||||
unused_qualifications)]
|
||||
#![doc(html_root_url = "https://docs.rs/lettre/0.9.3")]
|
||||
#![deny(
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces
|
||||
)]
|
||||
#[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;
|
||||
extern crate fast_chemail;
|
||||
#[cfg(feature = "connection-pool")]
|
||||
extern crate r2d2;
|
||||
#[cfg(feature = "file-transport")]
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub mod smtp;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub mod sendmail;
|
||||
pub mod stub;
|
||||
pub mod error;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
|
||||
use error::EmailResult;
|
||||
use error::Error;
|
||||
use fast_chemail::is_valid_email;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub use file::FileEmailTransport;
|
||||
pub use file::FileTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub use sendmail::SendmailTransport;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use smtp::{ClientSecurity, SmtpTransport};
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use smtp::client::net::ClientTlsParameters;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
|
||||
pub use smtp::r2d2::SmtpConnectionManager;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use smtp::{ClientSecurity, SmtpClient, SmtpTransport};
|
||||
use std::ffi::OsStr;
|
||||
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> {
|
||||
// TODO make some basic sanity checks
|
||||
if !is_valid_email(&address) && !address.ends_with("localhost") {
|
||||
return Err(Error::InvalidEmailAddress);
|
||||
}
|
||||
Ok(EmailAddress(address))
|
||||
}
|
||||
}
|
||||
@@ -115,6 +93,18 @@ 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).
|
||||
@@ -150,130 +140,74 @@ impl Envelope {
|
||||
pub fn from(&self) -> Option<&EmailAddress> {
|
||||
self.reverse_path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new builder
|
||||
pub fn builder() -> EnvelopeBuilder {
|
||||
EnvelopeBuilder::new()
|
||||
pub enum Message {
|
||||
Reader(Box<dyn 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
/// Sendable email structure
|
||||
pub struct SendableEmail {
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Message,
|
||||
}
|
||||
|
||||
impl EnvelopeBuilder {
|
||||
/// Constructs an envelope with no recipients and an empty sender
|
||||
pub fn new() -> Self {
|
||||
EnvelopeBuilder {
|
||||
to: vec![],
|
||||
from: None,
|
||||
impl SendableEmail {
|
||||
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
|
||||
SendableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Bytes(Cursor::new(message)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a recipient
|
||||
pub fn to<S: Into<EmailAddress>>(mut self, address: S) -> Self {
|
||||
self.add_to(address);
|
||||
self
|
||||
pub fn new_with_reader(
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Box<dyn Read + Send>,
|
||||
) -> SendableEmail {
|
||||
SendableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Reader(message),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a recipient
|
||||
pub fn add_to<S: Into<EmailAddress>>(&mut self, address: S) {
|
||||
self.to.push(address.into());
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
}
|
||||
|
||||
/// Sets the sender
|
||||
pub fn from<S: Into<EmailAddress>>(mut self, address: S) -> Self {
|
||||
self.set_from(address);
|
||||
self
|
||||
pub fn message_id(&self) -> &str {
|
||||
&self.message_id
|
||||
}
|
||||
|
||||
/// Sets the sender
|
||||
pub fn set_from<S: Into<EmailAddress>>(&mut self, address: S) {
|
||||
self.from = Some(address.into());
|
||||
pub fn message(self) -> Message {
|
||||
self.message
|
||||
}
|
||||
|
||||
/// Build the envelope
|
||||
pub fn build(self) -> EmailResult<Envelope> {
|
||||
Envelope::new(self.from, self.to)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 EmailTransport<'a, U: Read + 'a, V> {
|
||||
pub trait Transport<'a> {
|
||||
/// Result type for the transport
|
||||
type Result;
|
||||
|
||||
/// Sends the email
|
||||
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())
|
||||
}
|
||||
fn send(&mut self, email: SendableEmail) -> Self::Result;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//! Error and result type for sendmail transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::io;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
@@ -16,19 +18,15 @@ pub enum Error {
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
match *self {
|
||||
Client(ref err) => err.fmt(fmt),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Client(err) => err,
|
||||
Io(ref err) => err.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
@@ -38,13 +36,13 @@ impl StdError for Error {
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Io(err)
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Client(string)
|
||||
Error::Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
//! The sendmail transport sends the email using the local sendmail command.
|
||||
//!
|
||||
|
||||
use {EmailTransport, SendableEmail};
|
||||
use sendmail::error::SendmailResult;
|
||||
use std::io::Read;
|
||||
use std::io::prelude::*;
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
pub mod error;
|
||||
|
||||
@@ -32,22 +33,24 @@ impl SendmailTransport {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
impl<'a> Transport<'a> for SendmailTransport {
|
||||
type Result = SendmailResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> SendmailResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
// 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)
|
||||
.args(&[
|
||||
"-i",
|
||||
"-f",
|
||||
&match envelope.from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "\"\"".to_string(),
|
||||
},
|
||||
&to_addresses.join(" "),
|
||||
])
|
||||
.arg("-i")
|
||||
.arg("-f")
|
||||
.arg(
|
||||
email
|
||||
.envelope()
|
||||
.from()
|
||||
.map(|x| x.as_ref())
|
||||
.unwrap_or("\"\""),
|
||||
)
|
||||
.args(email.envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
@@ -55,26 +58,21 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTranspo
|
||||
let mut message_content = String::new();
|
||||
let _ = email.message().read_to_string(&mut message_content);
|
||||
|
||||
match process
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(message_content.as_bytes())
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(error) => return Err(From::from(error)),
|
||||
}
|
||||
.write_all(message_content.as_bytes())?;
|
||||
|
||||
info!("Wrote message to stdin");
|
||||
info!("Wrote {} message to stdin", message_id);
|
||||
|
||||
if let Ok(output) = process.wait_with_output() {
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(From::from("The message could not be sent"))
|
||||
}
|
||||
let output = process.wait_with_output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(From::from("The sendmail process stopped"))
|
||||
// TODO display stderr
|
||||
Err(error::Error::Client("The message could not be sent"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,13 @@
|
||||
//! Provides authentication mechanisms
|
||||
//! Provides limited SASL 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
|
||||
@@ -51,14 +33,17 @@ 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 {
|
||||
username: String,
|
||||
password: String,
|
||||
authentication_identity: String,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Create a `Credentials` struct from username and password
|
||||
pub fn new(username: String, password: String) -> Credentials {
|
||||
Credentials { username, password }
|
||||
Credentials {
|
||||
authentication_identity: username,
|
||||
secret: password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +58,9 @@ pub enum Mechanism {
|
||||
/// Obsolete but needed for some providers (like office365)
|
||||
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
|
||||
Login,
|
||||
/// CRAM-MD5 authentication mechanism
|
||||
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
CramMd5,
|
||||
/// Non-standard XOAUTH2 mechanism
|
||||
/// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||
Xoauth2,
|
||||
}
|
||||
|
||||
impl Display for Mechanism {
|
||||
@@ -87,8 +71,7 @@ impl Display for Mechanism {
|
||||
match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
Mechanism::CramMd5 => "CRAM-MD5",
|
||||
Mechanism::Xoauth2 => "XOAUTH2",
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -96,64 +79,49 @@ impl Display for Mechanism {
|
||||
|
||||
impl Mechanism {
|
||||
/// Does the mechanism supports initial response
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
|
||||
pub fn supports_initial_response(&self) -> bool {
|
||||
match *self {
|
||||
Mechanism::Plain => true,
|
||||
pub fn supports_initial_response(self) -> bool {
|
||||
match self {
|
||||
Mechanism::Plain | Mechanism::Xoauth2 => 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!(
|
||||
"{}{}{}{}",
|
||||
NUL, credentials.username, NUL, credentials.password
|
||||
"\u{0}{}\u{0}{}",
|
||||
credentials.authentication_identity, credentials.secret
|
||||
)),
|
||||
},
|
||||
Mechanism::Login => {
|
||||
let decoded_challenge = match challenge {
|
||||
Some(challenge) => challenge,
|
||||
None => return Err(Error::Client("This mechanism does expect a challenge")),
|
||||
};
|
||||
let decoded_challenge =
|
||||
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
|
||||
|
||||
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.username.to_string());
|
||||
return Ok(credentials.authentication_identity.to_string());
|
||||
}
|
||||
|
||||
if vec!["Password", "Password:"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.password.to_string());
|
||||
return Ok(credentials.secret.to_string());
|
||||
}
|
||||
|
||||
Err(Error::Client("Unrecognized challenge"))
|
||||
}
|
||||
#[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())
|
||||
))
|
||||
}
|
||||
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
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,21 +161,18 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
fn test_cram_md5() {
|
||||
let mechanism = Mechanism::CramMd5;
|
||||
fn test_xoauth2() {
|
||||
let mechanism = Mechanism::Xoauth2;
|
||||
|
||||
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
|
||||
let credentials = Credentials::new(
|
||||
"username".to_string(),
|
||||
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
mechanism
|
||||
.response(
|
||||
&credentials,
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")
|
||||
)
|
||||
.unwrap(),
|
||||
"alice a540ebe4ef2304070bbc3c456c1f64c0"
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, None).is_err());
|
||||
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
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::*;
|
||||
@@ -14,8 +13,8 @@ use std::net::ToSocketAddrs;
|
||||
use std::string::String;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod net;
|
||||
pub mod mock;
|
||||
pub mod net;
|
||||
|
||||
/// The codec used for transparency
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
@@ -72,12 +71,12 @@ impl ClientCodec {
|
||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||
/// Used for debug displays
|
||||
fn escape_crlf(string: &str) -> String {
|
||||
string.replace(CRLF, "<CRLF>")
|
||||
string.replace("\r\n", "<CRLF>")
|
||||
}
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Client<S: Write + Read = NetworkStream> {
|
||||
pub struct InnerClient<S: Write + Read = NetworkStream> {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: Option<BufStream<S>>,
|
||||
@@ -89,17 +88,17 @@ macro_rules! return_err (
|
||||
})
|
||||
);
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(new_without_default_derive))]
|
||||
impl<S: Write + Read> Client<S> {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default_derive))]
|
||||
impl<S: Write + Read> InnerClient<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Client`
|
||||
pub fn new() -> Client<S> {
|
||||
Client { stream: None }
|
||||
pub fn new() -> InnerClient<S> {
|
||||
InnerClient { stream: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
|
||||
/// Closes the SMTP transaction if possible
|
||||
pub fn close(&mut self) {
|
||||
let _ = self.command(QuitCommand);
|
||||
@@ -166,9 +165,9 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
|
||||
pub fn is_connected(&mut self) -> bool {
|
||||
self.command(NoopCommand).is_ok()
|
||||
self.stream.is_some() && self.command(NoopCommand).is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
@@ -194,10 +193,11 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message<T: Read>(&mut self, mut message: Box<T>) -> SmtpResult {
|
||||
pub fn message(&mut self, message: Box<dyn Read>) -> SmtpResult {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
let mut message_reader = BufReader::new(message.as_mut());
|
||||
|
||||
let mut message_reader = BufReader::new(message);
|
||||
|
||||
loop {
|
||||
out_buf.clear();
|
||||
@@ -218,7 +218,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
self.write(out_buf.as_slice())?;
|
||||
}
|
||||
|
||||
self.write(MESSAGE_ENDING.as_bytes())?;
|
||||
self.write(b"\r\n.\r\n")?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
@@ -254,7 +254,13 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
break;
|
||||
}
|
||||
// TODO read more than one line
|
||||
self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
|
||||
let read_count = self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
|
||||
|
||||
// EOF is reached
|
||||
if read_count == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
response = raw_response.parse::<Response>();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ impl ClientTlsParameters {
|
||||
}
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 compared to tls-native defaults.
|
||||
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv11, Protocol::Tlsv12];
|
||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[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(match_same_arms))]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::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(match_same_arms))]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn is_encrypted(&self) -> bool {
|
||||
match *self {
|
||||
NetworkStream::Tcp(_) => false,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#![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::{MailParameter, RcptParameter};
|
||||
use smtp::extension::ClientId;
|
||||
use smtp::extension::{MailParameter, RcptParameter};
|
||||
use smtp::response::Response;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use EmailAddress;
|
||||
|
||||
/// EHLO command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
@@ -19,8 +20,7 @@ pub struct EhloCommand {
|
||||
|
||||
impl Display for EhloCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "EHLO {}", self.client_id)?;
|
||||
f.write_str(CRLF)
|
||||
write!(f, "EHLO {}\r\n", self.client_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,7 @@ pub struct StarttlsCommand;
|
||||
|
||||
impl Display for StarttlsCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("STARTTLS")?;
|
||||
f.write_str(CRLF)
|
||||
f.write_str("STARTTLS\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,15 +55,12 @@ impl Display for MailCommand {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
match self.sender {
|
||||
Some(ref address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
self.sender.as_ref().map(|x| x.as_ref()).unwrap_or("")
|
||||
)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
}
|
||||
f.write_str(CRLF)
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +85,7 @@ impl Display for RcptCommand {
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
}
|
||||
f.write_str(CRLF)
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +106,7 @@ pub struct DataCommand;
|
||||
|
||||
impl Display for DataCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("DATA")?;
|
||||
f.write_str(CRLF)
|
||||
f.write_str("DATA\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +117,7 @@ pub struct QuitCommand;
|
||||
|
||||
impl Display for QuitCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("QUIT")?;
|
||||
f.write_str(CRLF)
|
||||
f.write_str("QUIT\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +128,7 @@ pub struct NoopCommand;
|
||||
|
||||
impl Display for NoopCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("NOOP")?;
|
||||
f.write_str(CRLF)
|
||||
f.write_str("NOOP\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +145,7 @@ impl Display for HelpCommand {
|
||||
if self.argument.is_some() {
|
||||
write!(f, " {}", self.argument.as_ref().unwrap())?;
|
||||
}
|
||||
f.write_str(CRLF)
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +165,8 @@ pub struct VrfyCommand {
|
||||
|
||||
impl Display for VrfyCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "VRFY {}", self.argument)?;
|
||||
f.write_str(CRLF)
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
|
||||
write!(f, "VRFY {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,8 +186,7 @@ pub struct ExpnCommand {
|
||||
|
||||
impl Display for ExpnCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "EXPN {}", self.argument)?;
|
||||
f.write_str(CRLF)
|
||||
write!(f, "EXPN {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,8 +204,7 @@ pub struct RsetCommand;
|
||||
|
||||
impl Display for RsetCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("RSET")?;
|
||||
f.write_str(CRLF)
|
||||
f.write_str("RSET\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,24 +220,20 @@ pub struct AuthCommand {
|
||||
|
||||
impl Display for AuthCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let encoded_response = if self.response.is_some() {
|
||||
Some(base64::encode_config(
|
||||
self.response.as_ref().unwrap().as_bytes(),
|
||||
base64::STANDARD,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let encoded_response = self
|
||||
.response
|
||||
.as_ref()
|
||||
.map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
|
||||
|
||||
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(CRLF)
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,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
|
||||
};
|
||||
@@ -281,21 +268,12 @@ impl AuthCommand {
|
||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||
}
|
||||
|
||||
let encoded_challenge = match response.first_word() {
|
||||
Some(challenge) => challenge.to_string(),
|
||||
None => return Err(Error::ResponseParsing("Could not read auth challenge")),
|
||||
};
|
||||
|
||||
let encoded_challenge = response
|
||||
.first_word()
|
||||
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
|
||||
debug!("auth encoded challenge: {}", 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)),
|
||||
};
|
||||
|
||||
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
|
||||
debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
|
||||
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
||||
@@ -313,8 +291,6 @@ 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() {
|
||||
@@ -391,18 +367,6 @@ 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!(
|
||||
"{}",
|
||||
@@ -410,24 +374,5 @@ 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,47 +37,41 @@ pub enum Error {
|
||||
/// TLS error
|
||||
Tls(native_tls::Error),
|
||||
/// Parsing error
|
||||
Parsing(nom::simple_errors::Err),
|
||||
Parsing(nom::ErrorKind),
|
||||
}
|
||||
|
||||
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(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),
|
||||
Io(ref err) => Some(&*err),
|
||||
Tls(ref err) => Some(&*err),
|
||||
Parsing(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -95,12 +89,24 @@ impl From<native_tls::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::simple_errors::Err> for Error {
|
||||
fn from(err: nom::simple_errors::Err) -> Error {
|
||||
impl From<nom::ErrorKind> for Error {
|
||||
fn from(err: nom::ErrorKind) -> Error {
|
||||
Parsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -10,8 +10,8 @@ use std::fmt::{self, Display, Formatter};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::result::Result;
|
||||
|
||||
/// Default ehlo clinet id
|
||||
pub const DEFAULT_EHLO_HOSTNAME: &str = "localhost";
|
||||
/// Default client id
|
||||
pub const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
|
||||
|
||||
/// Client identifier, the parameter to `EHLO`
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
@@ -44,10 +44,7 @@ impl ClientId {
|
||||
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
|
||||
/// found
|
||||
pub fn hostname() -> ClientId {
|
||||
ClientId::Domain(match get_hostname() {
|
||||
Some(name) => name,
|
||||
None => DEFAULT_EHLO_HOSTNAME.to_string(),
|
||||
})
|
||||
ClientId::Domain(get_hostname().unwrap_or_else(|| DEFAULT_DOMAIN_CLIENT_ID.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,21 +134,22 @@ impl ServerInfo {
|
||||
"STARTTLS" => {
|
||||
features.insert(Extension::StartTls);
|
||||
}
|
||||
"AUTH" => for &mechanism in &split[1..] {
|
||||
match mechanism {
|
||||
"PLAIN" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Plain));
|
||||
"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));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
"LOGIN" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Login));
|
||||
}
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
"CRAM-MD5" => {
|
||||
features.insert(Extension::Authentication(Mechanism::CramMd5));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
},
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
@@ -357,8 +355,6 @@ 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(
|
||||
@@ -368,7 +364,7 @@ mod test {
|
||||
),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
@@ -377,8 +373,7 @@ mod test {
|
||||
let mut features2 = HashSet::new();
|
||||
assert!(features2.insert(Extension::EightBitMime));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5),));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
|
||||
|
||||
let server_info2 = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
@@ -389,8 +384,6 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
//! CRAM-MD5 mechanisms
|
||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 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::client::Client;
|
||||
use smtp::authentication::{
|
||||
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
|
||||
};
|
||||
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 extension;
|
||||
pub mod commands;
|
||||
pub mod authentication;
|
||||
pub mod response;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "connection-pool")]
|
||||
pub mod r2d2;
|
||||
pub mod response;
|
||||
pub mod util;
|
||||
|
||||
// Registered port numbers:
|
||||
@@ -43,39 +43,22 @@ pub mod util;
|
||||
|
||||
/// Default smtp port
|
||||
pub const SMTP_PORT: u16 = 25;
|
||||
|
||||
/// Default submission port
|
||||
pub const SUBMISSION_PORT: u16 = 587;
|
||||
|
||||
// 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";
|
||||
/// Default submission over TLS port
|
||||
pub const SUBMISSIONS_PORT: u16 = 465;
|
||||
|
||||
/// How to apply TLS to a client connection
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub enum ClientSecurity {
|
||||
/// Insecure connection
|
||||
/// Insecure connection only (for testing purposes)
|
||||
None,
|
||||
/// Use `STARTTLS` when available
|
||||
/// Start with insecure connection and use `STARTTLS` when available
|
||||
Opportunistic(ClientTlsParameters),
|
||||
/// Always use `STARTTLS`
|
||||
/// Start with insecure connection and require `STARTTLS`
|
||||
Required(ClientTlsParameters),
|
||||
/// Use TLS wrapped connection without negotiation
|
||||
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
|
||||
/// Use TLS wrapped connection
|
||||
Wrapper(ClientTlsParameters),
|
||||
}
|
||||
|
||||
@@ -93,7 +76,8 @@ pub enum ConnectionReuseParameters {
|
||||
|
||||
/// Contains client configuration
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct SmtpTransportBuilder {
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpClient {
|
||||
/// Enable connection reuse
|
||||
connection_reuse: ConnectionReuseParameters,
|
||||
/// Name sent during EHLO
|
||||
@@ -114,7 +98,7 @@ pub struct SmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
impl SmtpTransportBuilder {
|
||||
impl SmtpClient {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
@@ -123,14 +107,13 @@ impl SmtpTransportBuilder {
|
||||
/// * No authentication
|
||||
/// * No SMTPUTF8 support
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
pub fn new<A: ToSocketAddrs>(
|
||||
addr: A,
|
||||
security: ClientSecurity,
|
||||
) -> Result<SmtpTransportBuilder, Error> {
|
||||
///
|
||||
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
|
||||
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
|
||||
let mut addresses = addr.to_socket_addrs()?;
|
||||
|
||||
match addresses.next() {
|
||||
Some(addr) => Ok(SmtpTransportBuilder {
|
||||
Some(addr) => Ok(SmtpClient {
|
||||
server_addr: addr,
|
||||
security,
|
||||
smtp_utf8: false,
|
||||
@@ -144,41 +127,59 @@ impl SmtpTransportBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) -> SmtpTransportBuilder {
|
||||
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
|
||||
self.smtp_utf8 = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> SmtpTransportBuilder {
|
||||
pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
|
||||
self.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable connection reuse
|
||||
pub fn connection_reuse(
|
||||
mut self,
|
||||
parameters: ConnectionReuseParameters,
|
||||
) -> SmtpTransportBuilder {
|
||||
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
|
||||
self.connection_reuse = parameters;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the client credentials
|
||||
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpTransportBuilder {
|
||||
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
|
||||
self.credentials = Some(credentials.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpTransportBuilder {
|
||||
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
|
||||
self.authentication_mechanism = Some(mechanism);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpTransportBuilder {
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
@@ -186,7 +187,7 @@ impl SmtpTransportBuilder {
|
||||
/// Build the SMTP client
|
||||
///
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
pub fn transport(self) -> SmtpTransport {
|
||||
SmtpTransport::new(self)
|
||||
}
|
||||
}
|
||||
@@ -209,9 +210,9 @@ pub struct SmtpTransport {
|
||||
/// SmtpTransport variable states
|
||||
state: State,
|
||||
/// Information about the client
|
||||
client_info: SmtpTransportBuilder,
|
||||
client_info: SmtpClient,
|
||||
/// Low level client
|
||||
client: Client,
|
||||
client: InnerClient,
|
||||
}
|
||||
|
||||
macro_rules! try_smtp (
|
||||
@@ -230,40 +231,11 @@ 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: SmtpTransportBuilder) -> SmtpTransport {
|
||||
let client = Client::new();
|
||||
pub fn new(builder: SmtpClient) -> SmtpTransport {
|
||||
let client = InnerClient::new();
|
||||
|
||||
SmtpTransport {
|
||||
client,
|
||||
@@ -276,6 +248,99 @@ 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
|
||||
@@ -306,101 +371,26 @@ impl<'a> SmtpTransport {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
||||
impl<'a> Transport<'a> for SmtpTransport {
|
||||
type Result = SmtpResult;
|
||||
|
||||
/// Sends an email
|
||||
#[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();
|
||||
#[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();
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
if !self.client.is_connected() {
|
||||
self.connect()?;
|
||||
}
|
||||
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self.server_info
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::EightBitMime)
|
||||
@@ -408,17 +398,21 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> 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(envelope.from().cloned(), mail_options,)),
|
||||
self.client.command(MailCommand::new(
|
||||
email.envelope().from().cloned(),
|
||||
mail_options,
|
||||
)),
|
||||
self
|
||||
);
|
||||
|
||||
@@ -426,17 +420,17 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
||||
info!(
|
||||
"{}: from=<{}>",
|
||||
message_id,
|
||||
match envelope.from() {
|
||||
match email.envelope().from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in envelope.to() {
|
||||
for to_address in email.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
|
||||
@@ -447,7 +441,7 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
||||
try_smtp!(self.client.command(DataCommand), self);
|
||||
|
||||
// Message content
|
||||
let result = self.client.message(email.message());
|
||||
let result = self.client.message(Box::new(email.message()));
|
||||
|
||||
if result.is_ok() {
|
||||
// Increment the connection reuse counter
|
||||
|
||||
38
lettre/src/smtp/r2d2.rs
Normal file
38
lettre/src/smtp/r2d2.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
//! SMTP response, containing a mandatory return code and an optional text
|
||||
//! message
|
||||
|
||||
use nom::{crlf, ErrorKind as NomErrorKind, IResult as NomResult};
|
||||
use nom::simple_errors::Err as NomError;
|
||||
use nom::{crlf, ErrorKind as NomErrorKind};
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
use std::result;
|
||||
use std::str::{FromStr, from_utf8};
|
||||
use std::str::{from_utf8, FromStr};
|
||||
|
||||
/// First digit indicates severity
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
@@ -126,13 +125,12 @@ pub struct Response {
|
||||
}
|
||||
|
||||
impl FromStr for Response {
|
||||
type Err = NomError;
|
||||
type Err = NomErrorKind;
|
||||
|
||||
fn from_str(s: &str) -> result::Result<Response, NomError> {
|
||||
fn from_str(s: &str) -> result::Result<Response, NomErrorKind> {
|
||||
match parse_response(s.as_bytes()) {
|
||||
NomResult::Done(_, res) => Ok(res),
|
||||
NomResult::Error(e) => Err(e),
|
||||
NomResult::Incomplete(_) => Err(NomErrorKind::Complete),
|
||||
Ok((_, res)) => Ok(res),
|
||||
Err(e) => Err(e.into_error_kind()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,20 +331,19 @@ 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,
|
||||
@@ -358,25 +355,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,
|
||||
@@ -388,7 +385,8 @@ mod test {
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).has_code(251));
|
||||
)
|
||||
.has_code(251));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -405,7 +403,8 @@ mod test {
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).first_word(),
|
||||
)
|
||||
.first_word(),
|
||||
Some("me")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -420,7 +419,8 @@ mod test {
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).first_word(),
|
||||
)
|
||||
.first_word(),
|
||||
Some("me")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -431,7 +431,8 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![],
|
||||
).first_word(),
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -442,7 +443,8 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
).first_word(),
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -453,7 +455,8 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
).first_word(),
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -464,7 +467,8 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec!["".to_string()],
|
||||
).first_word(),
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -483,7 +487,8 @@ mod test {
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).first_line(),
|
||||
)
|
||||
.first_line(),
|
||||
Some("me")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -498,7 +503,8 @@ mod test {
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).first_line(),
|
||||
)
|
||||
.first_line(),
|
||||
Some("me mo")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -509,7 +515,8 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![],
|
||||
).first_line(),
|
||||
)
|
||||
.first_line(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -520,7 +527,8 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
).first_line(),
|
||||
)
|
||||
.first_line(),
|
||||
Some(" ")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -531,7 +539,8 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
).first_line(),
|
||||
)
|
||||
.first_line(),
|
||||
Some(" ")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -542,7 +551,8 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec!["".to_string()],
|
||||
).first_line(),
|
||||
)
|
||||
.first_line(),
|
||||
Some("")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,42 +2,42 @@
|
||||
//! testing purposes.
|
||||
//!
|
||||
|
||||
use EmailTransport;
|
||||
use SendableEmail;
|
||||
use std::io::Read;
|
||||
use Transport;
|
||||
|
||||
/// This transport logs the message envelope and returns the given response
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct StubEmailTransport {
|
||||
pub struct StubTransport {
|
||||
response: StubResult,
|
||||
}
|
||||
|
||||
impl StubEmailTransport {
|
||||
impl StubTransport {
|
||||
/// Creates a new transport that always returns the given response
|
||||
pub fn new(response: StubResult) -> StubEmailTransport {
|
||||
StubEmailTransport { response }
|
||||
pub fn new(response: StubResult) -> StubTransport {
|
||||
StubTransport { response }
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_positive() -> StubEmailTransport {
|
||||
StubEmailTransport { response: Ok(()) }
|
||||
pub fn new_positive() -> StubTransport {
|
||||
StubTransport { response: Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type StubResult = Result<(), ()>;
|
||||
|
||||
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();
|
||||
impl<'a> Transport<'a> for StubTransport {
|
||||
type Result = StubResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> StubResult {
|
||||
info!(
|
||||
"{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
match envelope.from() {
|
||||
match email.envelope().from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
},
|
||||
envelope.to()
|
||||
email.envelope().to()
|
||||
);
|
||||
self.response
|
||||
}
|
||||
|
||||
74
lettre/tests/r2d2_smtp.rs
Normal file
74
lettre/tests/r2d2_smtp.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
extern crate glob;
|
||||
|
||||
use self::glob::glob;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::env;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn test_readme() {
|
||||
let readme = Path::new(file!())
|
||||
@@ -19,6 +20,7 @@ fn test_readme() {
|
||||
|
||||
skeptic_test(&readme);
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn book_test() {
|
||||
@@ -50,7 +52,8 @@ 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");
|
||||
|
||||
@@ -4,34 +4,38 @@ extern crate lettre;
|
||||
#[cfg(feature = "file-transport")]
|
||||
mod test {
|
||||
|
||||
use lettre::{EmailTransport, SendableEmail, SimpleSendableEmail};
|
||||
use lettre::file::FileEmailTransport;
|
||||
use lettre::file::FileTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
use std::env::temp_dir;
|
||||
use std::fs::File;
|
||||
use std::fs::remove_file;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
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);
|
||||
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);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let message_id = email.message_id();
|
||||
let file = format!("{}/{}.txt", temp_dir().to_str().unwrap(), message_id);
|
||||
let file = format!("{}/{}.json", 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\":\"file_id\",\"message\":[72,101,108,108,111,32,102,105,108,101]}"
|
||||
"{\"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]}"
|
||||
);
|
||||
|
||||
remove_file(file).unwrap();
|
||||
|
||||
@@ -3,21 +3,23 @@ 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 = SimpleSendableEmail::new(
|
||||
"user@localhost".to_string(),
|
||||
&["root@localhost".to_string()],
|
||||
"sendmail_id".to_string(),
|
||||
"Hello sendmail".to_string(),
|
||||
).unwrap();
|
||||
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 result = sender.send(&email);
|
||||
let result = sender.send(email);
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
@@ -3,22 +3,25 @@ extern crate lettre;
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
mod test {
|
||||
|
||||
use lettre::{ClientSecurity, EmailTransport, SimpleSendableEmail, SmtpTransport};
|
||||
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
#[test]
|
||||
fn smtp_transport_simple() {
|
||||
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.build();
|
||||
let email = SimpleSendableEmail::new(
|
||||
"user@localhost".to_string(),
|
||||
&["root@localhost".to_string()],
|
||||
"smtp_id".to_string(),
|
||||
"Hello smtp".to_string(),
|
||||
).unwrap();
|
||||
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(),
|
||||
);
|
||||
|
||||
sender.send(&email).unwrap();
|
||||
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.transport()
|
||||
.send(email)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{EmailTransport, SimpleSendableEmail};
|
||||
use lettre::stub::StubEmailTransport;
|
||||
use lettre::stub::StubTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
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();
|
||||
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(),
|
||||
);
|
||||
|
||||
sender_ok.send(&email).unwrap();
|
||||
sender_ko.send(&email).unwrap_err();
|
||||
sender_ok.send(email_ok).unwrap();
|
||||
sender_ko.send(email_ko).unwrap_err();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
|
||||
name = "lettre_email"
|
||||
version = "0.8.2" # 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"
|
||||
@@ -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.8", path = "../lettre", features = ["smtp-transport"] }
|
||||
glob = "0.2"
|
||||
lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
|
||||
glob = "0.3"
|
||||
|
||||
[dependencies]
|
||||
email = "^0.0"
|
||||
email = "^0.0.20"
|
||||
mime = "^0.3"
|
||||
time = "^0.1"
|
||||
uuid = { version = "^0.6", features = ["v4"] }
|
||||
lettre = { version = "^0.8", path = "../lettre", default-features = false }
|
||||
base64 = "^0.9"
|
||||
uuid = { version = "^0.7", features = ["v4"] }
|
||||
lettre = { version = "^0.9", path = "../lettre", default-features = false }
|
||||
base64 = "^0.10"
|
||||
|
||||
@@ -2,28 +2,27 @@ extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
extern crate mime;
|
||||
|
||||
use lettre::{EmailTransport, SmtpTransport};
|
||||
use lettre_email::EmailBuilder;
|
||||
use lettre::{SmtpClient, Transport};
|
||||
use lettre_email::Email;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let email = EmailBuilder::new()
|
||||
let email = Email::builder()
|
||||
// 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(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN).unwrap()
|
||||
.attachment_from_file(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost()
|
||||
.unwrap()
|
||||
.build();
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(&email);
|
||||
let result = mailer.send(email.into());
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
@@ -1,17 +1,18 @@
|
||||
//! Error and result type for emails
|
||||
|
||||
use self::Error::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::io;
|
||||
|
||||
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)]
|
||||
pub enum Error {
|
||||
/// Envelope error
|
||||
Email(lettre::Error),
|
||||
Envelope(lettre::error::Error),
|
||||
/// Unparseable filename for attachment
|
||||
CannotParseFilename,
|
||||
/// IO error
|
||||
@@ -19,29 +20,34 @@ pub enum Error {
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
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 description(&self) -> &str {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
match *self {
|
||||
Email(ref err) => err.description(),
|
||||
CannotParseFilename => "the attachment filename could not be parsed",
|
||||
Io(ref err) => err.description(),
|
||||
Envelope(ref err) => Some(err),
|
||||
Io(ref err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Io(err)
|
||||
Error::Io(err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre::Error> for Error {
|
||||
fn from(err: lettre::Error) -> Error {
|
||||
Email(err)
|
||||
impl From<lettre::error::Error> for Error {
|
||||
fn from(err: lettre::error::Error) -> Error {
|
||||
Error::Envelope(err)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
34
lettre_email/tests/build_with_envelope.rs
Normal file
34
lettre_email/tests/build_with_envelope.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
extern crate glob;
|
||||
|
||||
use self::glob::glob;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::env;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
@@ -36,7 +36,8 @@ 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
1
website/.gitignore
vendored
@@ -1,2 +1 @@
|
||||
node_modules
|
||||
/_book
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
all: depends _book
|
||||
|
||||
depends:
|
||||
gitbook install
|
||||
cargo install --force mdbook --vers "^0.2"
|
||||
cargo install --force mdbook-linkcheck --vers "^0.2"
|
||||
|
||||
serve:
|
||||
gitbook serve
|
||||
mdbook serve
|
||||
|
||||
_book:
|
||||
gitbook build
|
||||
mdbook build
|
||||
|
||||
clean:
|
||||
rm -rf _book/
|
||||
|
||||
.PHONY: _book
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"root": "./content",
|
||||
"plugins": [ "-sharing", "edit-link" ],
|
||||
"pluginsConfig": {
|
||||
"edit-link": {
|
||||
"base": "https://github.com/lettre/lettre/edit/master/website/src",
|
||||
"label": "Edit"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
website/book.toml
Normal file
11
website/book.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[book]
|
||||
title = "Lettre"
|
||||
author = "Alexis Mousset"
|
||||
description = "The user documentation of the Lettre crate."
|
||||
|
||||
[build]
|
||||
build-dir = "_book"
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.linkcheck]
|
||||
@@ -1,23 +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::{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());
|
||||
}
|
||||
```
|
||||
@@ -1,116 +0,0 @@
|
||||
SMTP Transport
|
||||
|
||||
This transport uses the SMTP protocol to send emails over the network (locally or remotely).
|
||||
|
||||
It is designed to be:
|
||||
|
||||
* Secured: email are encrypted by default
|
||||
* Modern: Unicode support for email content and sender/recipient addresses when compatible
|
||||
* Fast: supports tcp connection reuse
|
||||
|
||||
This client is designed to send emails to a relay server, and should *not* be used to send
|
||||
emails directly to the destination.
|
||||
|
||||
The relay server can be the local email server, a specific host or a third-party service.
|
||||
|
||||
#### Simple example
|
||||
|
||||
This is the most basic example of usage:
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{SimpleSendableEmail, EmailTransport, SmtpTransport};
|
||||
|
||||
fn main() {
|
||||
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 =
|
||||
SmtpTransport::builder_unencrypted_localhost().unwrap().build();
|
||||
// Send the email
|
||||
let result = mailer.send(&email);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
#### Complete example
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::{SimpleSendableEmail, EmailTransport, SmtpTransport};
|
||||
use lettre::smtp::extension::ClientId;
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
|
||||
fn main() {
|
||||
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 = 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
|
||||
.credentials(Credentials::new("username".to_string(), "password".to_string()))
|
||||
// Enable SMTPUTF8 if the server supports it
|
||||
.smtp_utf8(true)
|
||||
// Configure expected authentication mechanism
|
||||
.authentication_mechanism(Mechanism::Plain)
|
||||
// Enable connection reuse
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited).build();
|
||||
|
||||
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);
|
||||
assert!(result_2.is_ok());
|
||||
|
||||
// Explicitly close the SMTP transaction as we enabled connection reuse
|
||||
mailer.close();
|
||||
}
|
||||
```
|
||||
|
||||
#### Lower level
|
||||
|
||||
You can also send commands, here is a simple email transaction without
|
||||
error handling:
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::EmailAddress;
|
||||
use lettre::smtp::SMTP_PORT;
|
||||
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: 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(
|
||||
MailCommand::new(Some(EmailAddress::new("user@example.com".to_string()).unwrap()), vec![])
|
||||
);
|
||||
let _ = email_client.command(
|
||||
RcptCommand::new(EmailAddress::new("user@example.org".to_string()).unwrap(), vec![])
|
||||
);
|
||||
let _ = email_client.command(DataCommand);
|
||||
let _ = email_client.message(Box::new("Test email".as_bytes()));
|
||||
let _ = email_client.command(QuitCommand);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,30 +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::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>
|
||||
```
|
||||
@@ -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.20 or newer. Add the following to your `Cargo.toml`:
|
||||
Lettre requires Rust 1.32 or newer. Add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.8"
|
||||
lettre_email = "0.8"
|
||||
lettre = "0.9"
|
||||
lettre_email = "0.9"
|
||||
```
|
||||
@@ -10,17 +10,17 @@ An email is built using an `EmailBuilder`. The simplest email could be:
|
||||
```rust
|
||||
extern crate lettre_email;
|
||||
|
||||
use lettre_email::EmailBuilder;
|
||||
use lettre_email::Email;
|
||||
|
||||
fn main() {
|
||||
// Create an email
|
||||
let email = EmailBuilder::new()
|
||||
let email = Email::builder()
|
||||
// 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.")
|
||||
.alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.")
|
||||
.build();
|
||||
|
||||
assert!(email.is_ok());
|
||||
@@ -34,32 +34,3 @@ 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.
|
||||
|
||||
@@ -9,20 +9,22 @@ extern crate lettre;
|
||||
|
||||
use std::env::temp_dir;
|
||||
|
||||
use lettre::file::FileEmailTransport;
|
||||
use lettre::{SimpleSendableEmail, EmailTransport};
|
||||
use lettre::file::FileTransport;
|
||||
use lettre::{Transport, Envelope, EmailAddress, SendableEmail};
|
||||
|
||||
fn main() {
|
||||
// Write to the local temp directory
|
||||
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 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 result = sender.send(&email);
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
25
website/src/sending-messages/sendmail.md
Normal file
25
website/src/sending-messages/sendmail.md
Normal file
@@ -0,0 +1,25 @@
|
||||
#### 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());
|
||||
}
|
||||
```
|
||||
180
website/src/sending-messages/smtp.md
Normal file
180
website/src/sending-messages/smtp.md
Normal file
@@ -0,0 +1,180 @@
|
||||
#### SMTP Transport
|
||||
|
||||
This transport uses the SMTP protocol to send emails over the network (locally or remotely).
|
||||
|
||||
It is designed to be:
|
||||
|
||||
* Secured: email are encrypted by default
|
||||
* Modern: Unicode support for email content and sender/recipient addresses when compatible
|
||||
* Fast: supports tcp connection reuse
|
||||
|
||||
This client is designed to send emails to a relay server, and should *not* be used to send
|
||||
emails directly to the destination.
|
||||
|
||||
The relay server can be the local email server, a specific host or a third-party service.
|
||||
|
||||
#### Simple example
|
||||
|
||||
This is the most basic example of usage:
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{SendableEmail, EmailAddress, Transport, Envelope, SmtpClient};
|
||||
|
||||
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(),
|
||||
);
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer =
|
||||
SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(email);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
#### Complete example
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::{SendableEmail, Envelope, EmailAddress, Transport, SmtpClient};
|
||||
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(),
|
||||
);
|
||||
|
||||
// Connect to a remote server on a custom port
|
||||
let mut mailer = SmtpClient::new_simple("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
|
||||
.credentials(Credentials::new("username".to_string(), "password".to_string()))
|
||||
// Enable SMTPUTF8 if the server supports it
|
||||
.smtp_utf8(true)
|
||||
// Configure expected authentication mechanism
|
||||
.authentication_mechanism(Mechanism::Plain)
|
||||
// Enable connection reuse
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
|
||||
|
||||
let result_1 = mailer.send(email_1);
|
||||
assert!(result_1.is_ok());
|
||||
|
||||
// The second email will use the same connection
|
||||
let result_2 = mailer.send(email_2);
|
||||
assert!(result_2.is_ok());
|
||||
|
||||
// Explicitly close the SMTP transaction as we enabled connection reuse
|
||||
mailer.close();
|
||||
}
|
||||
```
|
||||
|
||||
You can specify custom TLS settings:
|
||||
|
||||
```rust,no_run
|
||||
extern crate native_tls;
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
|
||||
use lettre::{
|
||||
ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
|
||||
SendableEmail, SmtpClient, Transport,
|
||||
};
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
|
||||
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 mut tls_builder = TlsConnector::builder();
|
||||
tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
|
||||
let tls_parameters =
|
||||
ClientTlsParameters::new(
|
||||
"smtp.example.com".to_string(),
|
||||
tls_builder.build().unwrap()
|
||||
);
|
||||
|
||||
let mut mailer = SmtpClient::new(
|
||||
("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters)
|
||||
).unwrap()
|
||||
.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);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
mailer.close();
|
||||
}
|
||||
```
|
||||
|
||||
#### Lower level
|
||||
|
||||
You can also send commands, here is a simple email transaction without
|
||||
error handling:
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::EmailAddress;
|
||||
use lettre::smtp::SMTP_PORT;
|
||||
use lettre::smtp::client::InnerClient;
|
||||
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 _ = email_client.connect(&("localhost", SMTP_PORT), None);
|
||||
let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
|
||||
let _ = email_client.command(
|
||||
MailCommand::new(Some(EmailAddress::new("user@example.com".to_string()).unwrap()), vec![])
|
||||
);
|
||||
let _ = email_client.command(
|
||||
RcptCommand::new(EmailAddress::new("user@example.org".to_string()).unwrap(), vec![])
|
||||
);
|
||||
let _ = email_client.command(DataCommand);
|
||||
let _ = email_client.message(Box::new("Test email".as_bytes()));
|
||||
let _ = email_client.command(QuitCommand);
|
||||
}
|
||||
```
|
||||
|
||||
32
website/src/sending-messages/stub.md
Normal file
32
website/src/sending-messages/stub.md
Normal file
@@ -0,0 +1,32 @@
|
||||
#### 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>
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user