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
|
cd website
|
||||||
make clean && make
|
make clean && make
|
||||||
echo "lettre.at" > _book/CNAME
|
echo "lettre.at" > _book/html/CNAME
|
||||||
sudo pip install ghp-import
|
sudo pip install ghp-import
|
||||||
ghp-import -n _book
|
ghp-import -n _book/html
|
||||||
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
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
|
- stable
|
||||||
- beta
|
- beta
|
||||||
- nightly
|
- nightly
|
||||||
- 1.20.0
|
- 1.32.0
|
||||||
matrix:
|
matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- rust: nightly
|
- rust: nightly
|
||||||
@@ -19,13 +19,11 @@ addons:
|
|||||||
- gcc
|
- gcc
|
||||||
- binutils-dev
|
- binutils-dev
|
||||||
- libiberty-dev
|
- libiberty-dev
|
||||||
- npm
|
|
||||||
before_script:
|
before_script:
|
||||||
- smtp-sink 2525 1000&
|
- smtp-sink 2525 1000&
|
||||||
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
|
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
|
||||||
- sudo npm set strict-ssl false && sudo npm install -g gitbook-cli
|
|
||||||
script:
|
script:
|
||||||
- cargo test --verbose --all
|
- cargo test --verbose --all --all-features
|
||||||
after_success:
|
after_success:
|
||||||
- ./.build-scripts/codecov.sh
|
- ./.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>
|
<a name="v0.8.2"></a>
|
||||||
### v0.8.2 (2018-05-03)
|
### v0.8.2 (2018-05-03)
|
||||||
|
|
||||||
|
|||||||
@@ -34,19 +34,18 @@ Lettre provides the following features:
|
|||||||
|
|
||||||
## Example
|
## 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`:
|
To use this library, add the following to your `Cargo.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lettre = "0.8"
|
lettre = "0.9"
|
||||||
lettre_email = "0.8"
|
lettre_email = "0.9"
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
extern crate lettre;
|
extern crate lettre;
|
||||||
extern crate lettre_email;
|
extern crate lettre_email;
|
||||||
extern crate mime;
|
|
||||||
|
|
||||||
use lettre::{EmailTransport, SmtpTransport};
|
use lettre::{EmailTransport, SmtpTransport};
|
||||||
use lettre_email::EmailBuilder;
|
use lettre_email::EmailBuilder;
|
||||||
@@ -60,7 +59,6 @@ fn main() {
|
|||||||
.from("user@example.com")
|
.from("user@example.com")
|
||||||
.subject("Hi, Hello world")
|
.subject("Hi, Hello world")
|
||||||
.text("Hello world.")
|
.text("Hello world.")
|
||||||
.attachment(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN).unwrap()
|
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
|
|
||||||
name = "lettre"
|
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"
|
description = "Email client"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "http://lettre.at"
|
homepage = "http://lettre.at"
|
||||||
@@ -20,31 +20,34 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
nom = { version = "^3.2", optional = true }
|
nom = { version = "^4.0", optional = true }
|
||||||
bufstream = { version = "^0.1", optional = true }
|
bufstream = { version = "^0.1", optional = true }
|
||||||
native-tls = { version = "^0.1", optional = true }
|
native-tls = { version = "^0.2", optional = true }
|
||||||
base64 = { version = "^0.9", optional = true }
|
base64 = { version = "^0.10", optional = true }
|
||||||
hex = { version = "^0.3", optional = true }
|
|
||||||
hostname = { version = "^0.1", 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 = { version = "^1.0", optional = true }
|
||||||
serde_json = { version = "^1.0", optional = true }
|
serde_json = { version = "^1.0", optional = true }
|
||||||
serde_derive = { version = "^1.0", optional = true }
|
serde_derive = { version = "^1.0", optional = true }
|
||||||
|
fast_chemail = "^0.9"
|
||||||
|
r2d2 = { version = "^0.8", optional = true}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "^0.5"
|
env_logger = "^0.6"
|
||||||
glob = "0.2"
|
glob = "0.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["file-transport", "crammd5-auth", "smtp-transport", "sendmail-transport"]
|
default = ["file-transport", "smtp-transport", "sendmail-transport"]
|
||||||
unstable = []
|
unstable = []
|
||||||
serde-impls = ["serde", "serde_derive"]
|
serde-impls = ["serde", "serde_derive"]
|
||||||
file-transport = ["serde-impls", "serde_json"]
|
file-transport = ["serde-impls", "serde_json"]
|
||||||
crammd5-auth = ["md-5", "hmac", "hex"]
|
|
||||||
smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"]
|
smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"]
|
||||||
sendmail-transport = []
|
sendmail-transport = []
|
||||||
|
connection-pool = [ "r2d2" ]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "smtp"
|
name = "smtp"
|
||||||
required-features = ["smtp-transport"]
|
required-features = ["smtp-transport"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "smtp_gmail"
|
||||||
|
required-features = ["smtp-transport"]
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
extern crate lettre;
|
extern crate lettre;
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
use lettre::{ClientSecurity, SmtpTransport};
|
|
||||||
use lettre::{EmailAddress, EmailTransport, SimpleSendableEmail};
|
|
||||||
use lettre::smtp::ConnectionReuseParameters;
|
use lettre::smtp::ConnectionReuseParameters;
|
||||||
|
use lettre::{ClientSecurity, Envelope, SmtpTransport};
|
||||||
|
use lettre::{EmailAddress, SendableEmail, Transport};
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_simple_send(b: &mut test::Bencher) {
|
fn bench_simple_send(b: &mut test::Bencher) {
|
||||||
@@ -13,13 +13,16 @@ fn bench_simple_send(b: &mut test::Bencher) {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.build();
|
.build();
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let email = SimpleSendableEmail::new(
|
let email = SendableEmail::new(
|
||||||
EmailAddress::new("user@localhost".to_string()),
|
Envelope::new(
|
||||||
vec![EmailAddress::new("root@localhost".to_string())],
|
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||||
|
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
"id".to_string(),
|
"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());
|
assert!(result.is_ok());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -31,13 +34,16 @@ fn bench_reuse_send(b: &mut test::Bencher) {
|
|||||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
||||||
.build();
|
.build();
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let email = SimpleSendableEmail::new(
|
let email = SendableEmail::new(
|
||||||
EmailAddress::new("user@localhost".to_string()),
|
Envelope::new(
|
||||||
vec![EmailAddress::new("root@localhost".to_string())],
|
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||||
|
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
"id".to_string(),
|
"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());
|
assert!(result.is_ok());
|
||||||
});
|
});
|
||||||
sender.close()
|
sender.close()
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
extern crate env_logger;
|
extern crate env_logger;
|
||||||
extern crate lettre;
|
extern crate lettre;
|
||||||
|
|
||||||
use lettre::{EmailTransport, SimpleSendableEmail, SmtpTransport};
|
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let email = SimpleSendableEmail::new(
|
let email = SendableEmail::new(
|
||||||
"user@localhost".to_string(),
|
Envelope::new(
|
||||||
&["root@localhost".to_string()],
|
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||||
"my-message-id".to_string(),
|
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||||
"Hello ß☺ example".to_string(),
|
)
|
||||||
).unwrap();
|
.unwrap(),
|
||||||
|
"id".to_string(),
|
||||||
|
"Hello ß☺ example".to_string().into_bytes(),
|
||||||
|
);
|
||||||
|
|
||||||
// Open a local connection on port 25
|
// Open a local connection on port 25
|
||||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost()
|
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||||
.unwrap()
|
|
||||||
.build();
|
|
||||||
// Send the email
|
// Send the email
|
||||||
let result = mailer.send(&email);
|
let result = mailer.send(email);
|
||||||
|
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
println!("Email sent");
|
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 self::Error::*;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::error::Error as StdError;
|
|
||||||
use std::fmt::{self, Display, Formatter};
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::{
|
||||||
|
error::Error as StdError,
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
|
};
|
||||||
|
|
||||||
/// An enum of all error kinds.
|
/// An enum of all error kinds.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -19,20 +21,16 @@ pub enum Error {
|
|||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::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 {
|
impl StdError for Error {
|
||||||
fn description(&self) -> &str {
|
fn cause(&self) -> Option<&dyn StdError> {
|
||||||
match *self {
|
|
||||||
Client(err) => err,
|
|
||||||
Io(ref err) => err.description(),
|
|
||||||
JsonSerialization(ref err) => err.description(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cause(&self) -> Option<&StdError> {
|
|
||||||
match *self {
|
match *self {
|
||||||
Io(ref err) => Some(&*err),
|
Io(ref err) => Some(&*err),
|
||||||
JsonSerialization(ref err) => Some(&*err),
|
JsonSerialization(ref err) => Some(&*err),
|
||||||
@@ -43,19 +41,19 @@ impl StdError for Error {
|
|||||||
|
|
||||||
impl From<io::Error> for Error {
|
impl From<io::Error> for Error {
|
||||||
fn from(err: io::Error) -> Error {
|
fn from(err: io::Error) -> Error {
|
||||||
Io(err)
|
Error::Io(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<serde_json::Error> for Error {
|
impl From<serde_json::Error> for Error {
|
||||||
fn from(err: serde_json::Error) -> Error {
|
fn from(err: serde_json::Error) -> Error {
|
||||||
JsonSerialization(err)
|
Error::JsonSerialization(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&'static str> for Error {
|
impl From<&'static str> for Error {
|
||||||
fn from(string: &'static str) -> 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.
|
//! 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 file::error::FileResult;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use Envelope;
|
||||||
|
use SendableEmail;
|
||||||
|
use Transport;
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
/// Writes the content and the envelope information to a file
|
/// Writes the content and the envelope information to a file
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||||
pub struct FileEmailTransport {
|
pub struct FileTransport {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileEmailTransport {
|
impl FileTransport {
|
||||||
/// Creates a new transport to the given directory
|
/// Creates a new transport to the given directory
|
||||||
pub fn new<P: AsRef<Path>>(path: P) -> FileEmailTransport {
|
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||||
let mut path_buf = PathBuf::new();
|
FileTransport {
|
||||||
path_buf.push(path);
|
path: PathBuf::from(path.as_ref()),
|
||||||
FileEmailTransport { path: path_buf }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: Read + 'a> EmailTransport<'a, T, FileResult> for FileEmailTransport {
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> FileResult {
|
#[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();
|
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 serialized = serde_json::to_string(&SerializableEmail {
|
||||||
|
envelope,
|
||||||
let mut message_content = String::new();
|
message_id,
|
||||||
let _ = email.message().read_to_string(&mut message_content);
|
message: email.message_to_string()?.as_bytes().to_vec(),
|
||||||
|
})?;
|
||||||
let simple_email = SimpleSendableEmail::new_with_envelope(
|
|
||||||
email.envelope().clone(),
|
|
||||||
email.message_id().to_string(),
|
|
||||||
message_content,
|
|
||||||
);
|
|
||||||
|
|
||||||
f.write_all(serde_json::to_string(&simple_email)?.as_bytes())?;
|
|
||||||
|
|
||||||
|
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,80 @@
|
|||||||
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
|
//! 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
|
//! This mailer contains the available transports for your emails.
|
||||||
//! emails have to implement `SendableEmail`.
|
|
||||||
//!
|
//!
|
||||||
|
|
||||||
#![doc(html_root_url = "https://docs.rs/lettre/0.8.2")]
|
#![doc(html_root_url = "https://docs.rs/lettre/0.9.3")]
|
||||||
#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts,
|
#![deny(
|
||||||
trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces,
|
missing_copy_implementations,
|
||||||
unused_qualifications)]
|
trivial_casts,
|
||||||
|
trivial_numeric_casts,
|
||||||
|
unsafe_code,
|
||||||
|
unstable_features,
|
||||||
|
unused_import_braces
|
||||||
|
)]
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
extern crate base64;
|
extern crate base64;
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
extern crate bufstream;
|
extern crate bufstream;
|
||||||
#[cfg(feature = "crammd5-auth")]
|
|
||||||
extern crate hex;
|
|
||||||
#[cfg(feature = "crammd5-auth")]
|
|
||||||
extern crate hmac;
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
extern crate hostname;
|
extern crate hostname;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
#[cfg(feature = "crammd5-auth")]
|
|
||||||
extern crate md5;
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
extern crate native_tls;
|
extern crate native_tls;
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate nom;
|
extern crate nom;
|
||||||
#[cfg(feature = "serde-impls")]
|
#[cfg(feature = "serde-impls")]
|
||||||
|
extern crate serde;
|
||||||
|
#[cfg(feature = "serde-impls")]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
|
extern crate fast_chemail;
|
||||||
|
#[cfg(feature = "connection-pool")]
|
||||||
|
extern crate r2d2;
|
||||||
#[cfg(feature = "file-transport")]
|
#[cfg(feature = "file-transport")]
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
pub mod error;
|
||||||
pub mod smtp;
|
|
||||||
#[cfg(feature = "sendmail-transport")]
|
|
||||||
pub mod sendmail;
|
|
||||||
pub mod stub;
|
|
||||||
#[cfg(feature = "file-transport")]
|
#[cfg(feature = "file-transport")]
|
||||||
pub mod file;
|
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")]
|
#[cfg(feature = "file-transport")]
|
||||||
pub use file::FileEmailTransport;
|
pub use file::FileTransport;
|
||||||
#[cfg(feature = "sendmail-transport")]
|
#[cfg(feature = "sendmail-transport")]
|
||||||
pub use sendmail::SendmailTransport;
|
pub use sendmail::SendmailTransport;
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
pub use smtp::{ClientSecurity, SmtpTransport};
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
|
||||||
pub use smtp::client::net::ClientTlsParameters;
|
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::fmt::{self, Display, Formatter};
|
||||||
|
use std::io;
|
||||||
|
use std::io::Cursor;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::error::Error as StdError;
|
|
||||||
use std::str::FromStr;
|
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
|
/// Email address
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||||
pub struct EmailAddress(String);
|
pub struct EmailAddress(String);
|
||||||
|
|
||||||
impl EmailAddress {
|
impl EmailAddress {
|
||||||
/// Creates a new `EmailAddress`. For now it makes no validation.
|
|
||||||
pub fn new(address: String) -> EmailResult<EmailAddress> {
|
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))
|
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
|
/// Simple email envelope representation
|
||||||
///
|
///
|
||||||
/// We only accept mailboxes, and do not support source routes (as per RFC).
|
/// 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> {
|
pub fn from(&self) -> Option<&EmailAddress> {
|
||||||
self.reverse_path.as_ref()
|
self.reverse_path.as_ref()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a new builder
|
pub enum Message {
|
||||||
pub fn builder() -> EnvelopeBuilder {
|
Reader(Box<dyn Read + Send>),
|
||||||
EnvelopeBuilder::new()
|
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
|
/// Sendable email structure
|
||||||
#[derive(PartialEq, Eq, Clone, Debug, Default)]
|
pub struct SendableEmail {
|
||||||
pub struct EnvelopeBuilder {
|
envelope: Envelope,
|
||||||
/// The envelope recipients' addresses
|
message_id: String,
|
||||||
to: Vec<EmailAddress>,
|
message: Message,
|
||||||
/// The envelope sender address
|
|
||||||
from: Option<EmailAddress>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EnvelopeBuilder {
|
impl SendableEmail {
|
||||||
/// Constructs an envelope with no recipients and an empty sender
|
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
|
||||||
pub fn new() -> Self {
|
SendableEmail {
|
||||||
EnvelopeBuilder {
|
envelope,
|
||||||
to: vec![],
|
message_id,
|
||||||
from: None,
|
message: Message::Bytes(Cursor::new(message)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a recipient
|
pub fn new_with_reader(
|
||||||
pub fn to<S: Into<EmailAddress>>(mut self, address: S) -> Self {
|
envelope: Envelope,
|
||||||
self.add_to(address);
|
message_id: String,
|
||||||
self
|
message: Box<dyn Read + Send>,
|
||||||
|
) -> SendableEmail {
|
||||||
|
SendableEmail {
|
||||||
|
envelope,
|
||||||
|
message_id,
|
||||||
|
message: Message::Reader(message),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a recipient
|
pub fn envelope(&self) -> &Envelope {
|
||||||
pub fn add_to<S: Into<EmailAddress>>(&mut self, address: S) {
|
&self.envelope
|
||||||
self.to.push(address.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the sender
|
pub fn message_id(&self) -> &str {
|
||||||
pub fn from<S: Into<EmailAddress>>(mut self, address: S) -> Self {
|
&self.message_id
|
||||||
self.set_from(address);
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the sender
|
pub fn message(self) -> Message {
|
||||||
pub fn set_from<S: Into<EmailAddress>>(&mut self, address: S) {
|
self.message
|
||||||
self.from = Some(address.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the envelope
|
pub fn message_to_string(mut self) -> Result<String, io::Error> {
|
||||||
pub fn build(self) -> EmailResult<Envelope> {
|
let mut message_content = String::new();
|
||||||
Envelope::new(self.from, self.to)
|
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
|
/// 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
|
/// Sends the email
|
||||||
fn send<T: SendableEmail<'a, U> + 'a>(&mut self, email: &'a T) -> V;
|
fn send(&mut self, email: SendableEmail) -> Self::Result;
|
||||||
}
|
|
||||||
|
|
||||||
/// 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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
//! Error and result type for sendmail transport
|
//! Error and result type for sendmail transport
|
||||||
|
|
||||||
use self::Error::*;
|
use self::Error::*;
|
||||||
use std::error::Error as StdError;
|
|
||||||
use std::fmt::{self, Display, Formatter};
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::{
|
||||||
|
error::Error as StdError,
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
|
};
|
||||||
|
|
||||||
/// An enum of all error kinds.
|
/// An enum of all error kinds.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -16,19 +18,15 @@ pub enum Error {
|
|||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::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 {
|
impl StdError for Error {
|
||||||
fn description(&self) -> &str {
|
fn cause(&self) -> Option<&dyn StdError> {
|
||||||
match *self {
|
|
||||||
Client(err) => err,
|
|
||||||
Io(ref err) => err.description(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cause(&self) -> Option<&StdError> {
|
|
||||||
match *self {
|
match *self {
|
||||||
Io(ref err) => Some(&*err),
|
Io(ref err) => Some(&*err),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -38,13 +36,13 @@ impl StdError for Error {
|
|||||||
|
|
||||||
impl From<io::Error> for Error {
|
impl From<io::Error> for Error {
|
||||||
fn from(err: io::Error) -> Error {
|
fn from(err: io::Error) -> Error {
|
||||||
Io(err)
|
Error::Io(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&'static str> for Error {
|
impl From<&'static str> for Error {
|
||||||
fn from(string: &'static str) -> 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.
|
//! The sendmail transport sends the email using the local sendmail command.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use {EmailTransport, SendableEmail};
|
|
||||||
use sendmail::error::SendmailResult;
|
use sendmail::error::SendmailResult;
|
||||||
use std::io::Read;
|
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
|
use std::io::Read;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
use SendableEmail;
|
||||||
|
use Transport;
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
@@ -32,22 +33,24 @@ impl SendmailTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTransport {
|
impl<'a> Transport<'a> for SendmailTransport {
|
||||||
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SendmailResult {
|
type Result = SendmailResult;
|
||||||
let envelope = email.envelope();
|
|
||||||
|
fn send(&mut self, email: SendableEmail) -> SendmailResult {
|
||||||
|
let message_id = email.message_id().to_string();
|
||||||
|
|
||||||
// Spawn the sendmail command
|
// 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)
|
let mut process = Command::new(&self.command)
|
||||||
.args(&[
|
.arg("-i")
|
||||||
"-i",
|
.arg("-f")
|
||||||
"-f",
|
.arg(
|
||||||
&match envelope.from() {
|
email
|
||||||
Some(address) => address.to_string(),
|
.envelope()
|
||||||
None => "\"\"".to_string(),
|
.from()
|
||||||
},
|
.map(|x| x.as_ref())
|
||||||
&to_addresses.join(" "),
|
.unwrap_or("\"\""),
|
||||||
])
|
)
|
||||||
|
.args(email.envelope.to())
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
@@ -55,26 +58,21 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTranspo
|
|||||||
let mut message_content = String::new();
|
let mut message_content = String::new();
|
||||||
let _ = email.message().read_to_string(&mut message_content);
|
let _ = email.message().read_to_string(&mut message_content);
|
||||||
|
|
||||||
match process
|
process
|
||||||
.stdin
|
.stdin
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write_all(message_content.as_bytes())
|
.write_all(message_content.as_bytes())?;
|
||||||
{
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(error) => return Err(From::from(error)),
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Wrote message to stdin");
|
info!("Wrote {} message to stdin", message_id);
|
||||||
|
|
||||||
if let Ok(output) = process.wait_with_output() {
|
let output = process.wait_with_output()?;
|
||||||
if output.status.success() {
|
|
||||||
Ok(())
|
if output.status.success() {
|
||||||
} else {
|
Ok(())
|
||||||
Err(From::from("The message could not be sent"))
|
|
||||||
}
|
|
||||||
} else {
|
} 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 smtp::error::Error;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
/// Accepted authentication mechanisms on an encrypted connection
|
/// Accepted authentication mechanisms on an encrypted connection
|
||||||
/// Trying LOGIN last as it is deprecated.
|
/// 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];
|
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||||
|
|
||||||
/// Accepted authentication mechanisms on an unencrypted connection
|
/// 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] = &[];
|
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
|
||||||
|
|
||||||
/// Convertable to user credentials
|
/// 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)]
|
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
username: String,
|
authentication_identity: String,
|
||||||
password: String,
|
secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Credentials {
|
impl Credentials {
|
||||||
/// Create a `Credentials` struct from username and password
|
/// Create a `Credentials` struct from username and password
|
||||||
pub fn new(username: String, password: String) -> Credentials {
|
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)
|
/// Obsolete but needed for some providers (like office365)
|
||||||
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
|
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
|
||||||
Login,
|
Login,
|
||||||
/// CRAM-MD5 authentication mechanism
|
/// Non-standard XOAUTH2 mechanism
|
||||||
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
/// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||||
#[cfg(feature = "crammd5-auth")]
|
Xoauth2,
|
||||||
CramMd5,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Mechanism {
|
impl Display for Mechanism {
|
||||||
@@ -87,8 +71,7 @@ impl Display for Mechanism {
|
|||||||
match *self {
|
match *self {
|
||||||
Mechanism::Plain => "PLAIN",
|
Mechanism::Plain => "PLAIN",
|
||||||
Mechanism::Login => "LOGIN",
|
Mechanism::Login => "LOGIN",
|
||||||
#[cfg(feature = "crammd5-auth")]
|
Mechanism::Xoauth2 => "XOAUTH2",
|
||||||
Mechanism::CramMd5 => "CRAM-MD5",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -96,64 +79,49 @@ impl Display for Mechanism {
|
|||||||
|
|
||||||
impl Mechanism {
|
impl Mechanism {
|
||||||
/// Does the mechanism supports initial response
|
/// Does the mechanism supports initial response
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
|
pub fn supports_initial_response(self) -> bool {
|
||||||
pub fn supports_initial_response(&self) -> bool {
|
match self {
|
||||||
match *self {
|
Mechanism::Plain | Mechanism::Xoauth2 => true,
|
||||||
Mechanism::Plain => true,
|
|
||||||
Mechanism::Login => false,
|
Mechanism::Login => false,
|
||||||
#[cfg(feature = "crammd5-auth")]
|
|
||||||
Mechanism::CramMd5 => false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the string to send to the server, using the provided username, password and
|
/// Returns the string to send to the server, using the provided username, password and
|
||||||
/// challenge in some cases
|
/// challenge in some cases
|
||||||
pub fn response(
|
pub fn response(
|
||||||
&self,
|
self,
|
||||||
credentials: &Credentials,
|
credentials: &Credentials,
|
||||||
challenge: Option<&str>,
|
challenge: Option<&str>,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
match *self {
|
match self {
|
||||||
Mechanism::Plain => match challenge {
|
Mechanism::Plain => match challenge {
|
||||||
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||||
None => Ok(format!(
|
None => Ok(format!(
|
||||||
"{}{}{}{}",
|
"\u{0}{}\u{0}{}",
|
||||||
NUL, credentials.username, NUL, credentials.password
|
credentials.authentication_identity, credentials.secret
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Mechanism::Login => {
|
Mechanism::Login => {
|
||||||
let decoded_challenge = match challenge {
|
let decoded_challenge =
|
||||||
Some(challenge) => challenge,
|
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
|
||||||
None => return Err(Error::Client("This mechanism does expect a challenge")),
|
|
||||||
};
|
|
||||||
|
|
||||||
if vec!["User Name", "Username:", "Username"].contains(&decoded_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) {
|
if vec!["Password", "Password:"].contains(&decoded_challenge) {
|
||||||
return Ok(credentials.password.to_string());
|
return Ok(credentials.secret.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(Error::Client("Unrecognized challenge"))
|
Err(Error::Client("Unrecognized challenge"))
|
||||||
}
|
}
|
||||||
#[cfg(feature = "crammd5-auth")]
|
Mechanism::Xoauth2 => match challenge {
|
||||||
Mechanism::CramMd5 => {
|
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||||
let decoded_challenge = match challenge {
|
None => Ok(format!(
|
||||||
Some(challenge) => challenge,
|
"user={}\x01auth=Bearer {}\x01\x01",
|
||||||
None => return Err(Error::Client("This mechanism does expect a challenge")),
|
credentials.authentication_identity, credentials.secret
|
||||||
};
|
)),
|
||||||
|
},
|
||||||
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())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,21 +161,18 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "crammd5-auth")]
|
fn test_xoauth2() {
|
||||||
fn test_cram_md5() {
|
let mechanism = Mechanism::Xoauth2;
|
||||||
let mechanism = Mechanism::CramMd5;
|
|
||||||
|
|
||||||
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
|
let credentials = Credentials::new(
|
||||||
|
"username".to_string(),
|
||||||
|
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mechanism
|
mechanism.response(&credentials, None).unwrap(),
|
||||||
.response(
|
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
|
||||||
&credentials,
|
|
||||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
"alice a540ebe4ef2304070bbc3c456c1f64c0"
|
|
||||||
);
|
);
|
||||||
assert!(mechanism.response(&credentials, None).is_err());
|
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use bufstream::BufStream;
|
use bufstream::BufStream;
|
||||||
use nom::ErrorKind as NomErrorKind;
|
use nom::ErrorKind as NomErrorKind;
|
||||||
use smtp::{CRLF, MESSAGE_ENDING};
|
|
||||||
use smtp::authentication::{Credentials, Mechanism};
|
use smtp::authentication::{Credentials, Mechanism};
|
||||||
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
|
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
|
||||||
use smtp::commands::*;
|
use smtp::commands::*;
|
||||||
@@ -14,8 +13,8 @@ use std::net::ToSocketAddrs;
|
|||||||
use std::string::String;
|
use std::string::String;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub mod net;
|
|
||||||
pub mod mock;
|
pub mod mock;
|
||||||
|
pub mod net;
|
||||||
|
|
||||||
/// The codec used for transparency
|
/// The codec used for transparency
|
||||||
#[derive(Default, Clone, Copy, Debug)]
|
#[derive(Default, Clone, Copy, Debug)]
|
||||||
@@ -72,12 +71,12 @@ impl ClientCodec {
|
|||||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||||
/// Used for debug displays
|
/// Used for debug displays
|
||||||
fn escape_crlf(string: &str) -> String {
|
fn escape_crlf(string: &str) -> String {
|
||||||
string.replace(CRLF, "<CRLF>")
|
string.replace("\r\n", "<CRLF>")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Structure that implements the SMTP client
|
/// Structure that implements the SMTP client
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Client<S: Write + Read = NetworkStream> {
|
pub struct InnerClient<S: Write + Read = NetworkStream> {
|
||||||
/// TCP stream between client and server
|
/// TCP stream between client and server
|
||||||
/// Value is None before connection
|
/// Value is None before connection
|
||||||
stream: Option<BufStream<S>>,
|
stream: Option<BufStream<S>>,
|
||||||
@@ -89,17 +88,17 @@ macro_rules! return_err (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(new_without_default_derive))]
|
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default_derive))]
|
||||||
impl<S: Write + Read> Client<S> {
|
impl<S: Write + Read> InnerClient<S> {
|
||||||
/// Creates a new SMTP client
|
/// Creates a new SMTP client
|
||||||
///
|
///
|
||||||
/// It does not connects to the server, but only creates the `Client`
|
/// It does not connects to the server, but only creates the `Client`
|
||||||
pub fn new() -> Client<S> {
|
pub fn new() -> InnerClient<S> {
|
||||||
Client { stream: None }
|
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
|
/// Closes the SMTP transaction if possible
|
||||||
pub fn close(&mut self) {
|
pub fn close(&mut self) {
|
||||||
let _ = self.command(QuitCommand);
|
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
|
/// 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 {
|
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
|
/// 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
|
/// 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 out_buf: Vec<u8> = vec![];
|
||||||
let mut codec = ClientCodec::new();
|
let mut codec = ClientCodec::new();
|
||||||
let mut message_reader = BufReader::new(message.as_mut());
|
|
||||||
|
let mut message_reader = BufReader::new(message);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
out_buf.clear();
|
out_buf.clear();
|
||||||
@@ -218,7 +218,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
|||||||
self.write(out_buf.as_slice())?;
|
self.write(out_buf.as_slice())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.write(MESSAGE_ENDING.as_bytes())?;
|
self.write(b"\r\n.\r\n")?;
|
||||||
self.read_response()
|
self.read_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +254,13 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// TODO read more than one line
|
// 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>();
|
response = raw_response.parse::<Response>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ impl ClientTlsParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Accepted protocols by default.
|
/// Accepted protocols by default.
|
||||||
/// This removes TLS 1.0 compared to tls-native defaults.
|
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||||
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv11, Protocol::Tlsv12];
|
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv12];
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// Represents the different types of underlying network streams
|
/// 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<()> {
|
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
|
||||||
*self = match *self {
|
*self = match *self {
|
||||||
NetworkStream::Tcp(ref mut stream) => match tls_parameters
|
NetworkStream::Tcp(ref mut stream) => match tls_parameters
|
||||||
@@ -134,7 +134,7 @@ impl Connector for NetworkStream {
|
|||||||
Ok(())
|
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 {
|
fn is_encrypted(&self) -> bool {
|
||||||
match *self {
|
match *self {
|
||||||
NetworkStream::Tcp(_) => false,
|
NetworkStream::Tcp(_) => false,
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
#![cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
|
||||||
|
|
||||||
//! SMTP commands
|
//! SMTP commands
|
||||||
|
|
||||||
use EmailAddress;
|
|
||||||
use base64;
|
use base64;
|
||||||
use smtp::CRLF;
|
|
||||||
use smtp::authentication::{Credentials, Mechanism};
|
use smtp::authentication::{Credentials, Mechanism};
|
||||||
use smtp::error::Error;
|
use smtp::error::Error;
|
||||||
use smtp::extension::{MailParameter, RcptParameter};
|
|
||||||
use smtp::extension::ClientId;
|
use smtp::extension::ClientId;
|
||||||
|
use smtp::extension::{MailParameter, RcptParameter};
|
||||||
use smtp::response::Response;
|
use smtp::response::Response;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use EmailAddress;
|
||||||
|
|
||||||
/// EHLO command
|
/// EHLO command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
@@ -19,8 +20,7 @@ pub struct EhloCommand {
|
|||||||
|
|
||||||
impl Display for EhloCommand {
|
impl Display for EhloCommand {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "EHLO {}", self.client_id)?;
|
write!(f, "EHLO {}\r\n", self.client_id)
|
||||||
f.write_str(CRLF)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,8 +38,7 @@ pub struct StarttlsCommand;
|
|||||||
|
|
||||||
impl Display for StarttlsCommand {
|
impl Display for StarttlsCommand {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str("STARTTLS")?;
|
f.write_str("STARTTLS\r\n")
|
||||||
f.write_str(CRLF)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,15 +55,12 @@ impl Display for MailCommand {
|
|||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"MAIL FROM:<{}>",
|
"MAIL FROM:<{}>",
|
||||||
match self.sender {
|
self.sender.as_ref().map(|x| x.as_ref()).unwrap_or("")
|
||||||
Some(ref address) => address.to_string(),
|
|
||||||
None => "".to_string(),
|
|
||||||
}
|
|
||||||
)?;
|
)?;
|
||||||
for parameter in &self.parameters {
|
for parameter in &self.parameters {
|
||||||
write!(f, " {}", parameter)?;
|
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 {
|
for parameter in &self.parameters {
|
||||||
write!(f, " {}", parameter)?;
|
write!(f, " {}", parameter)?;
|
||||||
}
|
}
|
||||||
f.write_str(CRLF)
|
f.write_str("\r\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +106,7 @@ pub struct DataCommand;
|
|||||||
|
|
||||||
impl Display for DataCommand {
|
impl Display for DataCommand {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str("DATA")?;
|
f.write_str("DATA\r\n")
|
||||||
f.write_str(CRLF)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +117,7 @@ pub struct QuitCommand;
|
|||||||
|
|
||||||
impl Display for QuitCommand {
|
impl Display for QuitCommand {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str("QUIT")?;
|
f.write_str("QUIT\r\n")
|
||||||
f.write_str(CRLF)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +128,7 @@ pub struct NoopCommand;
|
|||||||
|
|
||||||
impl Display for NoopCommand {
|
impl Display for NoopCommand {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str("NOOP")?;
|
f.write_str("NOOP\r\n")
|
||||||
f.write_str(CRLF)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +145,7 @@ impl Display for HelpCommand {
|
|||||||
if self.argument.is_some() {
|
if self.argument.is_some() {
|
||||||
write!(f, " {}", self.argument.as_ref().unwrap())?;
|
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 {
|
impl Display for VrfyCommand {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "VRFY {}", self.argument)?;
|
#[cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
|
||||||
f.write_str(CRLF)
|
write!(f, "VRFY {}\r\n", self.argument)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,8 +186,7 @@ pub struct ExpnCommand {
|
|||||||
|
|
||||||
impl Display for ExpnCommand {
|
impl Display for ExpnCommand {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "EXPN {}", self.argument)?;
|
write!(f, "EXPN {}\r\n", self.argument)
|
||||||
f.write_str(CRLF)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +204,7 @@ pub struct RsetCommand;
|
|||||||
|
|
||||||
impl Display for RsetCommand {
|
impl Display for RsetCommand {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str("RSET")?;
|
f.write_str("RSET\r\n")
|
||||||
f.write_str(CRLF)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,24 +220,20 @@ pub struct AuthCommand {
|
|||||||
|
|
||||||
impl Display for AuthCommand {
|
impl Display for AuthCommand {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
let encoded_response = if self.response.is_some() {
|
let encoded_response = self
|
||||||
Some(base64::encode_config(
|
.response
|
||||||
self.response.as_ref().unwrap().as_bytes(),
|
.as_ref()
|
||||||
base64::STANDARD,
|
.map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.mechanism.supports_initial_response() {
|
if self.mechanism.supports_initial_response() {
|
||||||
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap(),)?;
|
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
||||||
} else {
|
} else {
|
||||||
match encoded_response {
|
match encoded_response {
|
||||||
Some(response) => f.write_str(&response)?,
|
Some(response) => f.write_str(&response)?,
|
||||||
None => write!(f, "AUTH {}", self.mechanism)?,
|
None => write!(f, "AUTH {}", self.mechanism)?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.write_str(CRLF)
|
f.write_str("\r\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +245,7 @@ impl AuthCommand {
|
|||||||
challenge: Option<String>,
|
challenge: Option<String>,
|
||||||
) -> Result<AuthCommand, Error> {
|
) -> Result<AuthCommand, Error> {
|
||||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -281,21 +268,12 @@ impl AuthCommand {
|
|||||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let encoded_challenge = match response.first_word() {
|
let encoded_challenge = response
|
||||||
Some(challenge) => challenge.to_string(),
|
.first_word()
|
||||||
None => return Err(Error::ResponseParsing("Could not read auth challenge")),
|
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
|
||||||
};
|
|
||||||
|
|
||||||
debug!("auth encoded challenge: {}", encoded_challenge);
|
debug!("auth encoded challenge: {}", encoded_challenge);
|
||||||
|
|
||||||
let decoded_challenge = match base64::decode(&encoded_challenge) {
|
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
|
||||||
Ok(challenge) => match String::from_utf8(challenge) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(error) => return Err(Error::Utf8Parsing(error)),
|
|
||||||
},
|
|
||||||
Err(error) => return Err(Error::ChallengeParsing(error)),
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("auth decoded challenge: {}", decoded_challenge);
|
debug!("auth decoded challenge: {}", decoded_challenge);
|
||||||
|
|
||||||
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
||||||
@@ -313,8 +291,6 @@ impl AuthCommand {
|
|||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use smtp::extension::MailBodyParameter;
|
use smtp::extension::MailBodyParameter;
|
||||||
#[cfg(feature = "crammd5-auth")]
|
|
||||||
use smtp::response::{Category, Code, Detail, Severity};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
@@ -391,18 +367,6 @@ mod test {
|
|||||||
),
|
),
|
||||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
"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!(
|
assert_eq!(
|
||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -410,24 +374,5 @@ mod test {
|
|||||||
),
|
),
|
||||||
"AUTH LOGIN\r\n"
|
"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 error
|
||||||
Tls(native_tls::Error),
|
Tls(native_tls::Error),
|
||||||
/// Parsing error
|
/// Parsing error
|
||||||
Parsing(nom::simple_errors::Err),
|
Parsing(nom::ErrorKind),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::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 {
|
impl StdError for Error {
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
|
fn cause(&self) -> Option<&dyn StdError> {
|
||||||
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> {
|
|
||||||
match *self {
|
match *self {
|
||||||
ChallengeParsing(ref err) => Some(&*err),
|
ChallengeParsing(ref err) => Some(&*err),
|
||||||
Utf8Parsing(ref err) => Some(&*err),
|
Utf8Parsing(ref err) => Some(&*err),
|
||||||
Io(ref err) => Some(&*err),
|
Io(ref err) => Some(&*err),
|
||||||
Tls(ref err) => Some(&*err),
|
Tls(ref err) => Some(&*err),
|
||||||
Parsing(ref err) => Some(&*err),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,12 +89,24 @@ impl From<native_tls::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<nom::simple_errors::Err> for Error {
|
impl From<nom::ErrorKind> for Error {
|
||||||
fn from(err: nom::simple_errors::Err) -> Error {
|
fn from(err: nom::ErrorKind) -> Error {
|
||||||
Parsing(err)
|
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 {
|
impl From<Response> for Error {
|
||||||
fn from(response: Response) -> Error {
|
fn from(response: Response) -> Error {
|
||||||
match response.code.severity {
|
match response.code.severity {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ use std::fmt::{self, Display, Formatter};
|
|||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
|
|
||||||
/// Default ehlo clinet id
|
/// Default client id
|
||||||
pub const DEFAULT_EHLO_HOSTNAME: &str = "localhost";
|
pub const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
|
||||||
|
|
||||||
/// Client identifier, the parameter to `EHLO`
|
/// Client identifier, the parameter to `EHLO`
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[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
|
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
|
||||||
/// found
|
/// found
|
||||||
pub fn hostname() -> ClientId {
|
pub fn hostname() -> ClientId {
|
||||||
ClientId::Domain(match get_hostname() {
|
ClientId::Domain(get_hostname().unwrap_or_else(|| DEFAULT_DOMAIN_CLIENT_ID.to_string()))
|
||||||
Some(name) => name,
|
|
||||||
None => DEFAULT_EHLO_HOSTNAME.to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,21 +134,22 @@ impl ServerInfo {
|
|||||||
"STARTTLS" => {
|
"STARTTLS" => {
|
||||||
features.insert(Extension::StartTls);
|
features.insert(Extension::StartTls);
|
||||||
}
|
}
|
||||||
"AUTH" => for &mechanism in &split[1..] {
|
"AUTH" => {
|
||||||
match mechanism {
|
for &mechanism in &split[1..] {
|
||||||
"PLAIN" => {
|
match mechanism {
|
||||||
features.insert(Extension::Authentication(Mechanism::Plain));
|
"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::EightBitMime));
|
||||||
assert!(!server_info.supports_feature(Extension::StartTls));
|
assert!(!server_info.supports_feature(Extension::StartTls));
|
||||||
#[cfg(feature = "crammd5-auth")]
|
|
||||||
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
|
|
||||||
|
|
||||||
let response2 = Response::new(
|
let response2 = Response::new(
|
||||||
Code::new(
|
Code::new(
|
||||||
@@ -368,7 +364,7 @@ mod test {
|
|||||||
),
|
),
|
||||||
vec![
|
vec![
|
||||||
"me".to_string(),
|
"me".to_string(),
|
||||||
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
|
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
|
||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_string(),
|
||||||
],
|
],
|
||||||
@@ -377,8 +373,7 @@ mod test {
|
|||||||
let mut features2 = HashSet::new();
|
let mut features2 = HashSet::new();
|
||||||
assert!(features2.insert(Extension::EightBitMime));
|
assert!(features2.insert(Extension::EightBitMime));
|
||||||
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
|
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
|
||||||
#[cfg(feature = "crammd5-auth")]
|
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
|
||||||
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5),));
|
|
||||||
|
|
||||||
let server_info2 = ServerInfo {
|
let server_info2 = ServerInfo {
|
||||||
name: "me".to_string(),
|
name: "me".to_string(),
|
||||||
@@ -389,8 +384,6 @@ mod test {
|
|||||||
|
|
||||||
assert!(server_info2.supports_feature(Extension::EightBitMime));
|
assert!(server_info2.supports_feature(Extension::EightBitMime));
|
||||||
assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
|
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));
|
assert!(!server_info2.supports_feature(Extension::StartTls));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,33 +8,33 @@
|
|||||||
//! It implements the following extensions:
|
//! It implements the following extensions:
|
||||||
//!
|
//!
|
||||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and
|
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
||||||
//! CRAM-MD5 mechanisms
|
|
||||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use EmailTransport;
|
|
||||||
use SendableEmail;
|
|
||||||
use native_tls::TlsConnector;
|
use native_tls::TlsConnector;
|
||||||
use smtp::authentication::{Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS,
|
use smtp::authentication::{
|
||||||
DEFAULT_UNENCRYPTED_MECHANISMS};
|
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
|
||||||
use smtp::client::Client;
|
};
|
||||||
use smtp::client::net::ClientTlsParameters;
|
use smtp::client::net::ClientTlsParameters;
|
||||||
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
|
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
|
||||||
|
use smtp::client::InnerClient;
|
||||||
use smtp::commands::*;
|
use smtp::commands::*;
|
||||||
use smtp::error::{Error, SmtpResult};
|
use smtp::error::{Error, SmtpResult};
|
||||||
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
|
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
|
||||||
use std::io::Read;
|
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use {SendableEmail, Transport};
|
||||||
|
|
||||||
pub mod extension;
|
|
||||||
pub mod commands;
|
|
||||||
pub mod authentication;
|
pub mod authentication;
|
||||||
pub mod response;
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
pub mod commands;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod extension;
|
||||||
|
#[cfg(feature = "connection-pool")]
|
||||||
|
pub mod r2d2;
|
||||||
|
pub mod response;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
// Registered port numbers:
|
// Registered port numbers:
|
||||||
@@ -43,39 +43,22 @@ pub mod util;
|
|||||||
|
|
||||||
/// Default smtp port
|
/// Default smtp port
|
||||||
pub const SMTP_PORT: u16 = 25;
|
pub const SMTP_PORT: u16 = 25;
|
||||||
|
|
||||||
/// Default submission port
|
/// Default submission port
|
||||||
pub const SUBMISSION_PORT: u16 = 587;
|
pub const SUBMISSION_PORT: u16 = 587;
|
||||||
|
/// Default submission over TLS port
|
||||||
// Useful strings and characters
|
pub const SUBMISSIONS_PORT: u16 = 465;
|
||||||
|
|
||||||
/// The word separator for SMTP transactions
|
|
||||||
pub const SP: &str = " ";
|
|
||||||
|
|
||||||
/// The line ending for SMTP transactions (carriage return + line feed)
|
|
||||||
pub const CRLF: &str = "\r\n";
|
|
||||||
|
|
||||||
/// Colon
|
|
||||||
pub const COLON: &str = ":";
|
|
||||||
|
|
||||||
/// The ending of message content
|
|
||||||
pub const MESSAGE_ENDING: &str = "\r\n.\r\n";
|
|
||||||
|
|
||||||
/// NUL unicode character
|
|
||||||
pub const NUL: &str = "\0";
|
|
||||||
|
|
||||||
/// How to apply TLS to a client connection
|
/// How to apply TLS to a client connection
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub enum ClientSecurity {
|
pub enum ClientSecurity {
|
||||||
/// Insecure connection
|
/// Insecure connection only (for testing purposes)
|
||||||
None,
|
None,
|
||||||
/// Use `STARTTLS` when available
|
/// Start with insecure connection and use `STARTTLS` when available
|
||||||
Opportunistic(ClientTlsParameters),
|
Opportunistic(ClientTlsParameters),
|
||||||
/// Always use `STARTTLS`
|
/// Start with insecure connection and require `STARTTLS`
|
||||||
Required(ClientTlsParameters),
|
Required(ClientTlsParameters),
|
||||||
/// Use TLS wrapped connection without negotiation
|
/// Use TLS wrapped connection
|
||||||
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
|
|
||||||
Wrapper(ClientTlsParameters),
|
Wrapper(ClientTlsParameters),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +76,8 @@ pub enum ConnectionReuseParameters {
|
|||||||
|
|
||||||
/// Contains client configuration
|
/// Contains client configuration
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct SmtpTransportBuilder {
|
#[derive(Clone)]
|
||||||
|
pub struct SmtpClient {
|
||||||
/// Enable connection reuse
|
/// Enable connection reuse
|
||||||
connection_reuse: ConnectionReuseParameters,
|
connection_reuse: ConnectionReuseParameters,
|
||||||
/// Name sent during EHLO
|
/// Name sent during EHLO
|
||||||
@@ -114,7 +98,7 @@ pub struct SmtpTransportBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for the SMTP `SmtpTransport`
|
/// Builder for the SMTP `SmtpTransport`
|
||||||
impl SmtpTransportBuilder {
|
impl SmtpClient {
|
||||||
/// Creates a new SMTP client
|
/// Creates a new SMTP client
|
||||||
///
|
///
|
||||||
/// Defaults are:
|
/// Defaults are:
|
||||||
@@ -123,14 +107,13 @@ impl SmtpTransportBuilder {
|
|||||||
/// * No authentication
|
/// * No authentication
|
||||||
/// * No SMTPUTF8 support
|
/// * No SMTPUTF8 support
|
||||||
/// * A 60 seconds timeout for smtp commands
|
/// * A 60 seconds timeout for smtp commands
|
||||||
pub fn new<A: ToSocketAddrs>(
|
///
|
||||||
addr: A,
|
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
|
||||||
security: ClientSecurity,
|
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
|
||||||
) -> Result<SmtpTransportBuilder, Error> {
|
|
||||||
let mut addresses = addr.to_socket_addrs()?;
|
let mut addresses = addr.to_socket_addrs()?;
|
||||||
|
|
||||||
match addresses.next() {
|
match addresses.next() {
|
||||||
Some(addr) => Ok(SmtpTransportBuilder {
|
Some(addr) => Ok(SmtpClient {
|
||||||
server_addr: addr,
|
server_addr: addr,
|
||||||
security,
|
security,
|
||||||
smtp_utf8: false,
|
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
|
/// 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.smtp_utf8 = enabled;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the name used during EHLO
|
/// 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.hello_name = name;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable connection reuse
|
/// Enable connection reuse
|
||||||
pub fn connection_reuse(
|
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
|
||||||
mut self,
|
|
||||||
parameters: ConnectionReuseParameters,
|
|
||||||
) -> SmtpTransportBuilder {
|
|
||||||
self.connection_reuse = parameters;
|
self.connection_reuse = parameters;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the client credentials
|
/// 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.credentials = Some(credentials.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the authentication mechanism to use
|
/// 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.authentication_mechanism = Some(mechanism);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the timeout duration
|
/// 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.timeout = timeout;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -186,7 +187,7 @@ impl SmtpTransportBuilder {
|
|||||||
/// Build the SMTP client
|
/// Build the SMTP client
|
||||||
///
|
///
|
||||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
/// 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)
|
SmtpTransport::new(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,9 +210,9 @@ pub struct SmtpTransport {
|
|||||||
/// SmtpTransport variable states
|
/// SmtpTransport variable states
|
||||||
state: State,
|
state: State,
|
||||||
/// Information about the client
|
/// Information about the client
|
||||||
client_info: SmtpTransportBuilder,
|
client_info: SmtpClient,
|
||||||
/// Low level client
|
/// Low level client
|
||||||
client: Client,
|
client: InnerClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! try_smtp (
|
macro_rules! try_smtp (
|
||||||
@@ -230,40 +231,11 @@ macro_rules! try_smtp (
|
|||||||
);
|
);
|
||||||
|
|
||||||
impl<'a> SmtpTransport {
|
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
|
/// Creates a new SMTP client
|
||||||
///
|
///
|
||||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||||
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
|
pub fn new(builder: SmtpClient) -> SmtpTransport {
|
||||||
let client = Client::new();
|
let client = InnerClient::new();
|
||||||
|
|
||||||
SmtpTransport {
|
SmtpTransport {
|
||||||
client,
|
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
|
/// Gets the EHLO response and updates server information
|
||||||
fn ehlo(&mut self) -> SmtpResult {
|
fn ehlo(&mut self) -> SmtpResult {
|
||||||
// Extended Hello
|
// 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
|
/// Sends an email
|
||||||
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms, cyclomatic_complexity))]
|
#[cfg_attr(
|
||||||
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SmtpResult {
|
feature = "cargo-clippy",
|
||||||
// Extract email information
|
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
|
||||||
let message_id = email.message_id();
|
)]
|
||||||
let envelope = email.envelope();
|
fn send(&mut self, email: SendableEmail) -> SmtpResult {
|
||||||
|
let message_id = email.message_id().to_string();
|
||||||
|
|
||||||
// Check if the connection is still available
|
if !self.client.is_connected() {
|
||||||
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
|
self.connect()?;
|
||||||
self.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.state.connection_reuse_count == 0 {
|
|
||||||
self.client.connect(
|
|
||||||
&self.client_info.server_addr,
|
|
||||||
match self.client_info.security {
|
|
||||||
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
self.client.set_timeout(self.client_info.timeout)?;
|
|
||||||
|
|
||||||
// Log the connection
|
|
||||||
info!("connection established to {}", self.client_info.server_addr);
|
|
||||||
|
|
||||||
self.ehlo()?;
|
|
||||||
|
|
||||||
match (
|
|
||||||
&self.client_info.security.clone(),
|
|
||||||
self.server_info
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.supports_feature(Extension::StartTls),
|
|
||||||
) {
|
|
||||||
(&ClientSecurity::Required(_), false) => {
|
|
||||||
return Err(From::from("Could not encrypt connection, aborting"))
|
|
||||||
}
|
|
||||||
(&ClientSecurity::Opportunistic(_), false) => (),
|
|
||||||
(&ClientSecurity::None, _) => (),
|
|
||||||
(&ClientSecurity::Wrapper(_), _) => (),
|
|
||||||
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
|
|
||||||
| (&ClientSecurity::Required(ref tls_parameters), true) => {
|
|
||||||
try_smtp!(self.client.command(StarttlsCommand), self);
|
|
||||||
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
|
|
||||||
|
|
||||||
debug!("connection encrypted");
|
|
||||||
|
|
||||||
// Send EHLO again
|
|
||||||
self.ehlo()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.client_info.credentials.is_some() {
|
|
||||||
let mut found = false;
|
|
||||||
|
|
||||||
// Compute accepted mechanism
|
|
||||||
let accepted_mechanisms = match self.client_info.authentication_mechanism {
|
|
||||||
Some(mechanism) => vec![mechanism],
|
|
||||||
None => {
|
|
||||||
if self.client.is_encrypted() {
|
|
||||||
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
|
|
||||||
} else {
|
|
||||||
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for mechanism in accepted_mechanisms {
|
|
||||||
if self.server_info
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.supports_auth_mechanism(mechanism)
|
|
||||||
{
|
|
||||||
found = true;
|
|
||||||
try_smtp!(
|
|
||||||
self.client
|
|
||||||
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
|
|
||||||
self
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
info!("No supported authentication mechanisms available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mail
|
// Mail
|
||||||
let mut mail_options = vec![];
|
let mut mail_options = vec![];
|
||||||
|
|
||||||
if self.server_info
|
if self
|
||||||
|
.server_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.supports_feature(Extension::EightBitMime)
|
.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));
|
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.server_info
|
if self
|
||||||
|
.server_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.supports_feature(Extension::SmtpUtfEight) && self.client_info.smtp_utf8
|
.supports_feature(Extension::SmtpUtfEight)
|
||||||
|
&& self.client_info.smtp_utf8
|
||||||
{
|
{
|
||||||
mail_options.push(MailParameter::SmtpUtfEight);
|
mail_options.push(MailParameter::SmtpUtfEight);
|
||||||
}
|
}
|
||||||
|
|
||||||
try_smtp!(
|
try_smtp!(
|
||||||
self.client
|
self.client.command(MailCommand::new(
|
||||||
.command(MailCommand::new(envelope.from().cloned(), mail_options,)),
|
email.envelope().from().cloned(),
|
||||||
|
mail_options,
|
||||||
|
)),
|
||||||
self
|
self
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -426,17 +420,17 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
|||||||
info!(
|
info!(
|
||||||
"{}: from=<{}>",
|
"{}: from=<{}>",
|
||||||
message_id,
|
message_id,
|
||||||
match envelope.from() {
|
match email.envelope().from() {
|
||||||
Some(address) => address.to_string(),
|
Some(address) => address.to_string(),
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Recipient
|
// Recipient
|
||||||
for to_address in envelope.to() {
|
for to_address in email.envelope().to() {
|
||||||
try_smtp!(
|
try_smtp!(
|
||||||
self.client
|
self.client
|
||||||
.command(RcptCommand::new(to_address.clone(), vec![]),),
|
.command(RcptCommand::new(to_address.clone(), vec![])),
|
||||||
self
|
self
|
||||||
);
|
);
|
||||||
// Log the rcpt command
|
// 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);
|
try_smtp!(self.client.command(DataCommand), self);
|
||||||
|
|
||||||
// Message content
|
// Message content
|
||||||
let result = self.client.message(email.message());
|
let result = self.client.message(Box::new(email.message()));
|
||||||
|
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
// Increment the connection reuse counter
|
// 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
|
//! SMTP response, containing a mandatory return code and an optional text
|
||||||
//! message
|
//! message
|
||||||
|
|
||||||
use nom::{crlf, ErrorKind as NomErrorKind, IResult as NomResult};
|
use nom::{crlf, ErrorKind as NomErrorKind};
|
||||||
use nom::simple_errors::Err as NomError;
|
|
||||||
use std::fmt::{Display, Formatter, Result};
|
use std::fmt::{Display, Formatter, Result};
|
||||||
use std::result;
|
use std::result;
|
||||||
use std::str::{FromStr, from_utf8};
|
use std::str::{from_utf8, FromStr};
|
||||||
|
|
||||||
/// First digit indicates severity
|
/// First digit indicates severity
|
||||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
@@ -126,13 +125,12 @@ pub struct Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for 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()) {
|
match parse_response(s.as_bytes()) {
|
||||||
NomResult::Done(_, res) => Ok(res),
|
Ok((_, res)) => Ok(res),
|
||||||
NomResult::Error(e) => Err(e),
|
Err(e) => Err(e.into_error_kind()),
|
||||||
NomResult::Incomplete(_) => Err(NomErrorKind::Complete),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,20 +331,19 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_is_positive() {
|
fn test_response_is_positive() {
|
||||||
assert!(
|
assert!(Response::new(
|
||||||
Response::new(
|
Code {
|
||||||
Code {
|
severity: Severity::PositiveCompletion,
|
||||||
severity: Severity::PositiveCompletion,
|
category: Category::MailSystem,
|
||||||
category: Category::MailSystem,
|
detail: Detail::Zero,
|
||||||
detail: Detail::Zero,
|
},
|
||||||
},
|
vec![
|
||||||
vec![
|
"me".to_string(),
|
||||||
"me".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"8BITMIME".to_string(),
|
"SIZE 42".to_string(),
|
||||||
"SIZE 42".to_string(),
|
],
|
||||||
],
|
)
|
||||||
).is_positive()
|
.is_positive());
|
||||||
);
|
|
||||||
assert!(!Response::new(
|
assert!(!Response::new(
|
||||||
Code {
|
Code {
|
||||||
severity: Severity::TransientNegativeCompletion,
|
severity: Severity::TransientNegativeCompletion,
|
||||||
@@ -358,25 +355,25 @@ mod test {
|
|||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_string(),
|
||||||
],
|
],
|
||||||
).is_positive());
|
)
|
||||||
|
.is_positive());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_has_code() {
|
fn test_response_has_code() {
|
||||||
assert!(
|
assert!(Response::new(
|
||||||
Response::new(
|
Code {
|
||||||
Code {
|
severity: Severity::TransientNegativeCompletion,
|
||||||
severity: Severity::TransientNegativeCompletion,
|
category: Category::MailSystem,
|
||||||
category: Category::MailSystem,
|
detail: Detail::One,
|
||||||
detail: Detail::One,
|
},
|
||||||
},
|
vec![
|
||||||
vec![
|
"me".to_string(),
|
||||||
"me".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"8BITMIME".to_string(),
|
"SIZE 42".to_string(),
|
||||||
"SIZE 42".to_string(),
|
],
|
||||||
],
|
)
|
||||||
).has_code(451)
|
.has_code(451));
|
||||||
);
|
|
||||||
assert!(!Response::new(
|
assert!(!Response::new(
|
||||||
Code {
|
Code {
|
||||||
severity: Severity::TransientNegativeCompletion,
|
severity: Severity::TransientNegativeCompletion,
|
||||||
@@ -388,7 +385,8 @@ mod test {
|
|||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_string(),
|
||||||
],
|
],
|
||||||
).has_code(251));
|
)
|
||||||
|
.has_code(251));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -405,7 +403,8 @@ mod test {
|
|||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_string(),
|
||||||
],
|
],
|
||||||
).first_word(),
|
)
|
||||||
|
.first_word(),
|
||||||
Some("me")
|
Some("me")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -420,7 +419,8 @@ mod test {
|
|||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_string(),
|
||||||
],
|
],
|
||||||
).first_word(),
|
)
|
||||||
|
.first_word(),
|
||||||
Some("me")
|
Some("me")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -431,7 +431,8 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![],
|
vec![],
|
||||||
).first_word(),
|
)
|
||||||
|
.first_word(),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -442,7 +443,8 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![" ".to_string()],
|
vec![" ".to_string()],
|
||||||
).first_word(),
|
)
|
||||||
|
.first_word(),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -453,7 +455,8 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![" ".to_string()],
|
vec![" ".to_string()],
|
||||||
).first_word(),
|
)
|
||||||
|
.first_word(),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -464,7 +467,8 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec!["".to_string()],
|
vec!["".to_string()],
|
||||||
).first_word(),
|
)
|
||||||
|
.first_word(),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -483,7 +487,8 @@ mod test {
|
|||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_string(),
|
||||||
],
|
],
|
||||||
).first_line(),
|
)
|
||||||
|
.first_line(),
|
||||||
Some("me")
|
Some("me")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -498,7 +503,8 @@ mod test {
|
|||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_string(),
|
||||||
],
|
],
|
||||||
).first_line(),
|
)
|
||||||
|
.first_line(),
|
||||||
Some("me mo")
|
Some("me mo")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -509,7 +515,8 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![],
|
vec![],
|
||||||
).first_line(),
|
)
|
||||||
|
.first_line(),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -520,7 +527,8 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![" ".to_string()],
|
vec![" ".to_string()],
|
||||||
).first_line(),
|
)
|
||||||
|
.first_line(),
|
||||||
Some(" ")
|
Some(" ")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -531,7 +539,8 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![" ".to_string()],
|
vec![" ".to_string()],
|
||||||
).first_line(),
|
)
|
||||||
|
.first_line(),
|
||||||
Some(" ")
|
Some(" ")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -542,7 +551,8 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec!["".to_string()],
|
vec!["".to_string()],
|
||||||
).first_line(),
|
)
|
||||||
|
.first_line(),
|
||||||
Some("")
|
Some("")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,42 +2,42 @@
|
|||||||
//! testing purposes.
|
//! testing purposes.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use EmailTransport;
|
|
||||||
use SendableEmail;
|
use SendableEmail;
|
||||||
use std::io::Read;
|
use Transport;
|
||||||
|
|
||||||
/// This transport logs the message envelope and returns the given response
|
/// This transport logs the message envelope and returns the given response
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct StubEmailTransport {
|
pub struct StubTransport {
|
||||||
response: StubResult,
|
response: StubResult,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StubEmailTransport {
|
impl StubTransport {
|
||||||
/// Creates a new transport that always returns the given response
|
/// Creates a new transport that always returns the given response
|
||||||
pub fn new(response: StubResult) -> StubEmailTransport {
|
pub fn new(response: StubResult) -> StubTransport {
|
||||||
StubEmailTransport { response }
|
StubTransport { response }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new transport that always returns a success response
|
/// Creates a new transport that always returns a success response
|
||||||
pub fn new_positive() -> StubEmailTransport {
|
pub fn new_positive() -> StubTransport {
|
||||||
StubEmailTransport { response: Ok(()) }
|
StubTransport { response: Ok(()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SMTP result type
|
/// SMTP result type
|
||||||
pub type StubResult = Result<(), ()>;
|
pub type StubResult = Result<(), ()>;
|
||||||
|
|
||||||
impl<'a, T: Read + 'a> EmailTransport<'a, T, StubResult> for StubEmailTransport {
|
impl<'a> Transport<'a> for StubTransport {
|
||||||
fn send<U: SendableEmail<'a, T>>(&mut self, email: &'a U) -> StubResult {
|
type Result = StubResult;
|
||||||
let envelope = email.envelope();
|
|
||||||
|
fn send(&mut self, email: SendableEmail) -> StubResult {
|
||||||
info!(
|
info!(
|
||||||
"{}: from=<{}> to=<{:?}>",
|
"{}: from=<{}> to=<{:?}>",
|
||||||
email.message_id(),
|
email.message_id(),
|
||||||
match envelope.from() {
|
match email.envelope().from() {
|
||||||
Some(address) => address.to_string(),
|
Some(address) => address.to_string(),
|
||||||
None => "".to_string(),
|
None => "".to_string(),
|
||||||
},
|
},
|
||||||
envelope.to()
|
email.envelope().to()
|
||||||
);
|
);
|
||||||
self.response
|
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;
|
extern crate glob;
|
||||||
|
|
||||||
use self::glob::glob;
|
use self::glob::glob;
|
||||||
use std::env::consts::EXE_EXTENSION;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::env::consts::EXE_EXTENSION;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
/*
|
||||||
#[test]
|
#[test]
|
||||||
fn test_readme() {
|
fn test_readme() {
|
||||||
let readme = Path::new(file!())
|
let readme = Path::new(file!())
|
||||||
@@ -19,6 +20,7 @@ fn test_readme() {
|
|||||||
|
|
||||||
skeptic_test(&readme);
|
skeptic_test(&readme);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn book_test() {
|
fn book_test() {
|
||||||
@@ -50,7 +52,8 @@ fn skeptic_test(path: &Path) {
|
|||||||
.arg(&depdir)
|
.arg(&depdir)
|
||||||
.arg(path);
|
.arg(path);
|
||||||
|
|
||||||
let result = cmd.spawn()
|
let result = cmd
|
||||||
|
.spawn()
|
||||||
.expect("Failed to spawn process")
|
.expect("Failed to spawn process")
|
||||||
.wait()
|
.wait()
|
||||||
.expect("Failed to run process");
|
.expect("Failed to run process");
|
||||||
|
|||||||
@@ -4,34 +4,38 @@ extern crate lettre;
|
|||||||
#[cfg(feature = "file-transport")]
|
#[cfg(feature = "file-transport")]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
use lettre::{EmailTransport, SendableEmail, SimpleSendableEmail};
|
use lettre::file::FileTransport;
|
||||||
use lettre::file::FileEmailTransport;
|
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||||
use std::env::temp_dir;
|
use std::env::temp_dir;
|
||||||
use std::fs::File;
|
|
||||||
use std::fs::remove_file;
|
use std::fs::remove_file;
|
||||||
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn file_transport() {
|
fn file_transport() {
|
||||||
let mut sender = FileEmailTransport::new(temp_dir());
|
let mut sender = FileTransport::new(temp_dir());
|
||||||
let email = SimpleSendableEmail::new(
|
let email = SendableEmail::new(
|
||||||
"user@localhost".to_string(),
|
Envelope::new(
|
||||||
&["root@localhost".to_string()],
|
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||||
"file_id".to_string(),
|
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||||
"Hello file".to_string(),
|
)
|
||||||
).unwrap();
|
.unwrap(),
|
||||||
let result = sender.send(&email);
|
"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());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
let message_id = email.message_id();
|
let file = format!("{}/{}.json", temp_dir().to_str().unwrap(), message_id);
|
||||||
let file = format!("{}/{}.txt", temp_dir().to_str().unwrap(), message_id);
|
|
||||||
let mut f = File::open(file.clone()).unwrap();
|
let mut f = File::open(file.clone()).unwrap();
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
let _ = f.read_to_string(&mut buffer);
|
let _ = f.read_to_string(&mut buffer);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer,
|
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();
|
remove_file(file).unwrap();
|
||||||
|
|||||||
@@ -3,21 +3,23 @@ extern crate lettre;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(feature = "sendmail-transport")]
|
#[cfg(feature = "sendmail-transport")]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
use lettre::{EmailTransport, SimpleSendableEmail};
|
|
||||||
use lettre::sendmail::SendmailTransport;
|
use lettre::sendmail::SendmailTransport;
|
||||||
|
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sendmail_transport_simple() {
|
fn sendmail_transport_simple() {
|
||||||
let mut sender = SendmailTransport::new();
|
let mut sender = SendmailTransport::new();
|
||||||
let email = SimpleSendableEmail::new(
|
let email = SendableEmail::new(
|
||||||
"user@localhost".to_string(),
|
Envelope::new(
|
||||||
&["root@localhost".to_string()],
|
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||||
"sendmail_id".to_string(),
|
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||||
"Hello sendmail".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);
|
println!("{:?}", result);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,25 @@ extern crate lettre;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
mod test {
|
mod test {
|
||||||
|
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||||
use lettre::{ClientSecurity, EmailTransport, SimpleSendableEmail, SmtpTransport};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn smtp_transport_simple() {
|
fn smtp_transport_simple() {
|
||||||
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
|
let email = SendableEmail::new(
|
||||||
.unwrap()
|
Envelope::new(
|
||||||
.build();
|
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||||
let email = SimpleSendableEmail::new(
|
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||||
"user@localhost".to_string(),
|
)
|
||||||
&["root@localhost".to_string()],
|
.unwrap(),
|
||||||
"smtp_id".to_string(),
|
"id".to_string(),
|
||||||
"Hello smtp".to_string(),
|
"Hello ß☺ example".to_string().into_bytes(),
|
||||||
).unwrap();
|
);
|
||||||
|
|
||||||
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;
|
extern crate lettre;
|
||||||
|
|
||||||
use lettre::{EmailTransport, SimpleSendableEmail};
|
use lettre::stub::StubTransport;
|
||||||
use lettre::stub::StubEmailTransport;
|
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stub_transport() {
|
fn stub_transport() {
|
||||||
let mut sender_ok = StubEmailTransport::new_positive();
|
let mut sender_ok = StubTransport::new_positive();
|
||||||
let mut sender_ko = StubEmailTransport::new(Err(()));
|
let mut sender_ko = StubTransport::new(Err(()));
|
||||||
let email = SimpleSendableEmail::new(
|
let email_ok = SendableEmail::new(
|
||||||
"user@localhost".to_string(),
|
Envelope::new(
|
||||||
&["root@localhost".to_string()],
|
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||||
"stub_id".to_string(),
|
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||||
"Hello stub".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_ok.send(email_ok).unwrap();
|
||||||
sender_ko.send(&email).unwrap_err();
|
sender_ko.send(email_ko).unwrap_err();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
|
|
||||||
name = "lettre_email"
|
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"
|
description = "Email builder"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "http://lettre.at"
|
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" }
|
is-it-maintained-open-issues = { repository = "lettre/lettre_email" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lettre = { version = "^0.8", path = "../lettre", features = ["smtp-transport"] }
|
lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
|
||||||
glob = "0.2"
|
glob = "0.3"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
email = "^0.0"
|
email = "^0.0.20"
|
||||||
mime = "^0.3"
|
mime = "^0.3"
|
||||||
time = "^0.1"
|
time = "^0.1"
|
||||||
uuid = { version = "^0.6", features = ["v4"] }
|
uuid = { version = "^0.7", features = ["v4"] }
|
||||||
lettre = { version = "^0.8", path = "../lettre", default-features = false }
|
lettre = { version = "^0.9", path = "../lettre", default-features = false }
|
||||||
base64 = "^0.9"
|
base64 = "^0.10"
|
||||||
|
|||||||
@@ -2,28 +2,27 @@ extern crate lettre;
|
|||||||
extern crate lettre_email;
|
extern crate lettre_email;
|
||||||
extern crate mime;
|
extern crate mime;
|
||||||
|
|
||||||
use lettre::{EmailTransport, SmtpTransport};
|
use lettre::{SmtpClient, Transport};
|
||||||
use lettre_email::EmailBuilder;
|
use lettre_email::Email;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let email = EmailBuilder::new()
|
let email = Email::builder()
|
||||||
// Addresses can be specified by the tuple (email, alias)
|
// Addresses can be specified by the tuple (email, alias)
|
||||||
.to(("user@example.org", "Firstname Lastname"))
|
.to(("user@example.org", "Firstname Lastname"))
|
||||||
// ... or by an address only
|
// ... or by an address only
|
||||||
.from("user@example.com")
|
.from("user@example.com")
|
||||||
.subject("Hi, Hello world")
|
.subject("Hi, Hello world")
|
||||||
.text("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()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Open a local connection on port 25
|
// Open a local connection on port 25
|
||||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost()
|
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||||
.unwrap()
|
|
||||||
.build();
|
|
||||||
// Send the email
|
// Send the email
|
||||||
let result = mailer.send(&email);
|
let result = mailer.send(email.into());
|
||||||
|
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
println!("Email sent");
|
println!("Email sent");
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
//! Error and result type for emails
|
//! 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 lettre;
|
||||||
|
use std::io;
|
||||||
|
use std::{
|
||||||
|
error::Error as StdError,
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
|
};
|
||||||
|
use self::Error::*;
|
||||||
|
|
||||||
/// An enum of all error kinds.
|
/// An enum of all error kinds.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Envelope error
|
/// Envelope error
|
||||||
Email(lettre::Error),
|
Envelope(lettre::error::Error),
|
||||||
/// Unparseable filename for attachment
|
/// Unparseable filename for attachment
|
||||||
CannotParseFilename,
|
CannotParseFilename,
|
||||||
/// IO error
|
/// IO error
|
||||||
@@ -19,29 +20,34 @@ pub enum Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
impl Display for Error {
|
||||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
fmt.write_str(self.description())
|
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 {
|
impl StdError for Error {
|
||||||
fn description(&self) -> &str {
|
fn cause(&self) -> Option<&dyn StdError> {
|
||||||
match *self {
|
match *self {
|
||||||
Email(ref err) => err.description(),
|
Envelope(ref err) => Some(err),
|
||||||
CannotParseFilename => "the attachment filename could not be parsed",
|
Io(ref err) => Some(err),
|
||||||
Io(ref err) => err.description(),
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for Error {
|
impl From<io::Error> for Error {
|
||||||
fn from(err: io::Error) -> Error {
|
fn from(err: io::Error) -> Error {
|
||||||
Io(err)
|
Error::Io(err)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<lettre::Error> for Error {
|
impl From<lettre::error::Error> for Error {
|
||||||
fn from(err: lettre::Error) -> Error {
|
fn from(err: lettre::error::Error) -> Error {
|
||||||
Email(err)
|
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;
|
extern crate glob;
|
||||||
|
|
||||||
use self::glob::glob;
|
use self::glob::glob;
|
||||||
use std::env::consts::EXE_EXTENSION;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::env::consts::EXE_EXTENSION;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
@@ -36,7 +36,8 @@ fn skeptic_test(path: &Path) {
|
|||||||
.arg(&depdir)
|
.arg(&depdir)
|
||||||
.arg(path);
|
.arg(path);
|
||||||
|
|
||||||
let result = cmd.spawn()
|
let result = cmd
|
||||||
|
.spawn()
|
||||||
.expect("Failed to spawn process")
|
.expect("Failed to spawn process")
|
||||||
.wait()
|
.wait()
|
||||||
.expect("Failed to run process");
|
.expect("Failed to run process");
|
||||||
|
|||||||
1
website/.gitignore
vendored
1
website/.gitignore
vendored
@@ -1,2 +1 @@
|
|||||||
node_modules
|
|
||||||
/_book
|
/_book
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
all: depends _book
|
all: depends _book
|
||||||
|
|
||||||
depends:
|
depends:
|
||||||
gitbook install
|
cargo install --force mdbook --vers "^0.2"
|
||||||
|
cargo install --force mdbook-linkcheck --vers "^0.2"
|
||||||
|
|
||||||
serve:
|
serve:
|
||||||
gitbook serve
|
mdbook serve
|
||||||
|
|
||||||
_book:
|
_book:
|
||||||
gitbook build
|
mdbook build
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf _book/
|
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`
|
The `lettre_email` crate allows you to compose messages, and the `lettre`
|
||||||
provide transports to send them.
|
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
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lettre = "0.8"
|
lettre = "0.9"
|
||||||
lettre_email = "0.8"
|
lettre_email = "0.9"
|
||||||
```
|
```
|
||||||
@@ -10,17 +10,17 @@ An email is built using an `EmailBuilder`. The simplest email could be:
|
|||||||
```rust
|
```rust
|
||||||
extern crate lettre_email;
|
extern crate lettre_email;
|
||||||
|
|
||||||
use lettre_email::EmailBuilder;
|
use lettre_email::Email;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Create an email
|
// Create an email
|
||||||
let email = EmailBuilder::new()
|
let email = Email::builder()
|
||||||
// Addresses can be specified by the tuple (email, alias)
|
// Addresses can be specified by the tuple (email, alias)
|
||||||
.to(("user@example.org", "Firstname Lastname"))
|
.to(("user@example.org", "Firstname Lastname"))
|
||||||
// ... or by an address only
|
// ... or by an address only
|
||||||
.from("user@example.com")
|
.from("user@example.com")
|
||||||
.subject("Hi, Hello world")
|
.subject("Hi, Hello world")
|
||||||
.text("Hello world.")
|
.alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert!(email.is_ok());
|
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
|
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
|
HTML email. You can use the `alternative()` method to provide both versions, using plain text
|
||||||
as fallback for the HTML version.
|
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 std::env::temp_dir;
|
||||||
|
|
||||||
use lettre::file::FileEmailTransport;
|
use lettre::file::FileTransport;
|
||||||
use lettre::{SimpleSendableEmail, EmailTransport};
|
use lettre::{Transport, Envelope, EmailAddress, SendableEmail};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Write to the local temp directory
|
// Write to the local temp directory
|
||||||
let mut sender = FileEmailTransport::new(temp_dir());
|
let mut sender = FileTransport::new(temp_dir());
|
||||||
let email = SimpleSendableEmail::new(
|
let email = SendableEmail::new(
|
||||||
"user@localhost".to_string(),
|
Envelope::new(
|
||||||
&["root@localhost".to_string()],
|
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||||
"message_id".to_string(),
|
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||||
"Hello world".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());
|
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