Compare commits

...

39 Commits

Author SHA1 Message Date
Paolo Barbolini
8b6cee30ee Prepare 0.11.2 (#919) 2023-11-23 09:49:21 +01:00
Paolo Barbolini
62c16e90ef Bump idna to v0.5 (#918) 2023-11-23 08:23:25 +00:00
Paolo Barbolini
e0494a5f9d Bump boringssl crates to v4 (#915) 2023-11-19 11:49:43 +01:00
Paolo Barbolini
8c3bffa728 Bump MSRV to 1.70 (#916) 2023-11-19 11:42:49 +01:00
Paolo Barbolini
47eda90433 Prepare 0.11.1 (#910) 2023-10-24 23:47:49 +02:00
Paolo Barbolini
46ea8c48ac Ignore rustls deprecation warning 2023-10-24 22:55:16 +02:00
Paolo Barbolini
5f7063fdc3 Fix accidental disabling of webpki-roots setup (#909) 2023-10-24 22:54:22 +02:00
Paolo Barbolini
61c1f6bc6f Fix date in changelog 2023-10-15 17:22:48 +02:00
Paolo Barbolini
283e21f8d6 Prepare 0.11.0 (#899) 2023-10-15 16:21:32 +02:00
Paolo Barbolini
20c3701eb0 Fix doctests (#906) 2023-10-15 16:04:12 +02:00
Paolo Barbolini
74117d5cc6 ci: fix MSRV job (#905) 2023-09-23 09:16:42 +02:00
Marlon
bb49e0a46b Construct a SmtpTransport from a connection URL (#901) 2023-09-23 08:48:36 +02:00
Paolo Barbolini
42365478c2 Revert "Added Address:new_unchecked (#887)" (#904)
This reverts commit 7e6ffe8aea.
2023-09-01 16:06:48 +02:00
Hugo
94769242d1 docs: improve documentation for AsyncSmtpConnection (#903)
Closes: #902

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2023-08-25 10:55:56 +02:00
Wyatt Herkamp
7e6ffe8aea Added Address:new_unchecked (#887) 2023-08-19 00:11:47 +02:00
Luc Lenôtre
16c35ef583 added headers_mut function on Message (#889)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2023-08-18 23:39:38 +02:00
Alexis Mousset
bbab86b484 A few spelling and doc fixes (#900) 2023-08-16 21:56:33 +02:00
Paolo Barbolini
b5652f18b7 Fix -Z minimal-versions (#898) 2023-08-15 11:43:44 +02:00
Paolo Barbolini
c2f2b907a9 Bump boring crates to v3 (#897) 2023-08-15 11:26:08 +02:00
Edwin
a1cc770613 Fix RUSTSEC-2022-0093 (#896) 2023-08-15 10:50:03 +02:00
Paolo Barbolini
57886c367d Fix latest clippy warnings (#891) 2023-07-27 20:32:47 +02:00
Paolo Barbolini
f3a469431e Bump webpki-roots 0.25 (#890) 2023-07-27 08:02:31 +02:00
Paolo Barbolini
9b48ef355b Bump webpki-roots to v0.24 (#884) 2023-07-07 08:14:18 +02:00
Paolo Barbolini
7fee8dc5a8 ci: bump rustfmt (#883) 2023-06-23 09:12:28 +02:00
Paolo Barbolini
7e9fff9bd0 Bump dependencies (#882) 2023-06-23 09:12:09 +02:00
Paolo Barbolini
92f5460132 Bump MSRV to 1.65 (#881) 2023-06-23 07:11:57 +00:00
tecc
cd0c032f71 change: Add From<Address> implementation for Mailbox (#879)
from-address: It's a simple implementation - it uses the address as the address and uses `None` for the name parameter.
2023-06-22 10:22:41 +02:00
Paolo Barbolini
f41c9c19ab Cherry-pick 0.10.4 release changelog 2023-04-02 11:47:26 +02:00
Paolo Barbolini
cb6a7178d9 Bump socket2 to 0.5 (#868) 2023-04-02 11:34:05 +02:00
Paolo Barbolini
2bfc759aa3 ci: remove async-global-executor workaround (#870) 2023-04-02 11:30:50 +02:00
Paolo Barbolini
89673d0eb2 Bump MSRV to 1.63 (#869) 2023-04-02 11:20:51 +02:00
Paolo Barbolini
8b588cf275 Bump rustls to 0.21 (#867) 2023-04-02 10:53:54 +02:00
Clément DOUIN
5f37b66352 Improve mailbox parsing using chumsky (#839) 2023-02-20 14:09:23 +01:00
Paolo Barbolini
69e5974024 Hide internal optional dependencies using cargo's 1.60 dep: syntax (#861) 2023-02-20 12:00:32 +01:00
Paolo Barbolini
4fb67a7da1 Prepare 0.10.3 (#860) 2023-02-20 11:56:28 +01:00
Paolo Barbolini
9041f210f4 Add Content-Type to all examples sending a basic text/plain message (#859) 2023-02-14 17:54:05 +00:00
Paolo Barbolini
77b7d40fb8 mailbox: replace serialize_str(&self.to_string()) with collect_str(self) (#858) 2023-02-14 18:35:29 +01:00
Paolo Barbolini
2b6d457f85 clippy: deny str_to_string and empty_structs_with_brackets (#857) 2023-02-14 18:33:10 +01:00
Stéphan Kochen
952c1b39df Add support for rustls-native-certs (#843) 2023-02-14 18:11:42 +01:00
56 changed files with 1318 additions and 379 deletions

View File

@@ -13,7 +13,7 @@ env:
jobs: jobs:
rustfmt: rustfmt:
name: rustfmt / nightly-2022-11-12 name: rustfmt / nightly-2023-06-22
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -22,7 +22,7 @@ jobs:
- name: Install rust - name: Install rust
run: | run: |
rustup default nightly-2022-11-12 rustup default nightly-2023-06-22
rustup component add rustfmt rustup component add rustfmt
- name: cargo fmt - name: cargo fmt
@@ -75,8 +75,8 @@ jobs:
rust: stable rust: stable
- name: beta - name: beta
rust: beta rust: beta
- name: 1.60.0 - name: '1.70'
rust: 1.60.0 rust: '1.70'
steps: steps:
- name: Checkout - name: Checkout
@@ -113,7 +113,10 @@ jobs:
run: sudo apt -y install python3-dkim run: sudo apt -y install python3-dkim
- name: Work around early dependencies MSRV bump - name: Work around early dependencies MSRV bump
run: cargo update -p async-global-executor --precise 2.0.4 run: |
cargo update -p anstyle --precise 1.0.2
cargo update -p clap --precise 4.3.24
cargo update -p clap_lex --precise 0.5.0
- name: Test with no default features - name: Test with no default features
run: cargo test --no-default-features run: cargo test --no-default-features
@@ -122,10 +125,10 @@ jobs:
run: cargo test run: cargo test
- name: Test with all features (-native-tls) - name: Test with all features (-native-tls)
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,boring,boring-tls,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tokio1_boring,tokio1_crate,tokio1_rustls,tracing,uuid,webpki-roots run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tracing
- name: Test with all features (-boring-tls) - name: Test with all features (-boring-tls)
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,native-tls,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-native-tls,tokio1-rustls-tls,tokio1_crate,tokio1_native_tls_crate,tokio1_rustls,tracing,uuid,webpki-roots run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls-tls,tracing
# coverage: # coverage:
# name: Coverage # name: Coverage

View File

@@ -1,3 +1,107 @@
<a name="v0.11.2"></a>
### v0.11.2 (2023-11-23)
#### Upgrade notes
* MSRV is now 1.70 ([#916])
#### Misc
* Bump `idna` to v0.5 ([#918])
* Bump `boring` and `tokio-boring` to v4 ([#915])
[#915]: https://github.com/lettre/lettre/pull/915
[#916]: https://github.com/lettre/lettre/pull/916
[#918]: https://github.com/lettre/lettre/pull/918
<a name="v0.11.1"></a>
### v0.11.1 (2023-10-24)
#### Bug fixes
* Fix `webpki-roots` certificate store setup ([#909])
[#909]: https://github.com/lettre/lettre/pull/909
<a name="v0.11.0"></a>
### v0.11.0 (2023-10-15)
While this release technically contains breaking changes, we expect most projects
to be able to upgrade by only bumping the version in `Cargo.toml`.
#### Upgrade notes
* MSRV is now 1.65 ([#869] and [#881])
* `AddressError` is now marked as `#[non_exhaustive]` ([#839])
#### Features
* Improve mailbox parsing ([#839])
* Add construction of SMTP transport from URL ([#901])
* Add `From<Address>` implementation for `Mailbox` ([#879])
#### Misc
* Bump `socket2` to v0.5 ([#868])
* Bump `idna` to v0.4, `fastrand` to v2, `quoted_printable` to v0.5, `rsa` to v0.9 ([#882])
* Bump `webpki-roots` to v0.25 ([#884] and [#890])
* Bump `ed25519-dalek` to v2 fixing RUSTSEC-2022-0093 ([#896])
* Bump `boring`ssl crates to v3 ([#897])
[#839]: https://github.com/lettre/lettre/pull/839
[#868]: https://github.com/lettre/lettre/pull/868
[#869]: https://github.com/lettre/lettre/pull/869
[#879]: https://github.com/lettre/lettre/pull/879
[#881]: https://github.com/lettre/lettre/pull/881
[#882]: https://github.com/lettre/lettre/pull/882
[#884]: https://github.com/lettre/lettre/pull/884
[#890]: https://github.com/lettre/lettre/pull/890
[#896]: https://github.com/lettre/lettre/pull/896
[#897]: https://github.com/lettre/lettre/pull/897
[#901]: https://github.com/lettre/lettre/pull/901
<a name="v0.10.4"></a>
### v0.10.4 (2023-04-02)
#### Misc
* Bumped rustls to 0.21 and all related dependencies ([#867])
[#867]: https://github.com/lettre/lettre/pull/867
<a name="v0.10.3"></a>
### v0.10.3 (2023-02-20)
#### Announcements
It was found that what had been used until now as a basic lettre 0.10
`MessageBuilder::body` example failed to mention that for maximum
compatibility with various email clients a `Content-Type` header
should always be present in the message.
##### Before
```rust
Message::builder()
// [...] some headers skipped for brevity
.body(String::from("A plaintext or html body"))?
```
##### Patch
```diff
Message::builder()
// [...] some headers skipped for brevity
+ .header(ContentType::TEXT_PLAIN) // or `TEXT_HTML` if the body is html
.body(String::from("A plaintext or html body"))?
```
#### Features
* Add support for rustls-native-certs when using rustls ([#843])
[#843]: https://github.com/lettre/lettre/pull/843
<a name="v0.10.2"></a> <a name="v0.10.2"></a>
### v0.10.2 (2023-01-29) ### v0.10.2 (2023-01-29)

View File

@@ -1,6 +1,6 @@
## Contributing to Lettre ## Contributing to Lettre
The following guidelines are inspired from the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md). The following guidelines are inspired by the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
### Code formatting ### Code formatting

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge) # remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.2" version = "0.11.2"
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
homepage = "https://lettre.rs" homepage = "https://lettre.rs"
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
categories = ["email", "network-programming"] categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"] keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2021" edition = "2021"
rust-version = "1.60" rust-version = "1.70"
[badges] [badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" } is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -19,15 +19,16 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
idna = "0.3" chumsky = "0.9"
idna = "0.5"
once_cell = { version = "1", optional = true } once_cell = { version = "1", optional = true }
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder # builder
httpdate = { version = "1", optional = true } httpdate = { version = "1", optional = true }
mime = { version = "0.3.4", optional = true } mime = { version = "0.3.4", optional = true }
fastrand = { version = "1.4", optional = true } fastrand = { version = "2.0", optional = true }
quoted_printable = { version = "0.4.6", optional = true } quoted_printable = { version = "0.5", optional = true }
base64 = { version = "0.21", optional = true } base64 = { version = "0.21", optional = true }
email-encoding = { version = "0.2", optional = true } email-encoding = { version = "0.2", optional = true }
@@ -39,14 +40,16 @@ serde_json = { version = "1", optional = true }
# smtp-transport # smtp-transport
nom = { version = "7", optional = true } nom = { version = "7", optional = true }
hostname = { version = "0.3", optional = true } # feature hostname = { version = "0.3", optional = true } # feature
socket2 = { version = "0.4.4", optional = true } socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true }
## tls ## tls
native-tls = { version = "0.2", optional = true } # feature native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true } rustls = { version = "0.21", features = ["dangerous_configuration"], optional = true }
rustls-pemfile = { version = "1", optional = true } rustls-pemfile = { version = "1", optional = true }
webpki-roots = { version = "0.22", optional = true } rustls-native-certs = { version = "0.6.2", optional = true }
boring = { version = "2.0.0", optional = true } webpki-roots = { version = "0.25", optional = true }
boring = { version = "4", optional = true }
# async # async
futures-io = { version = "0.3.7", optional = true } futures-io = { version = "0.3.7", optional = true }
@@ -56,25 +59,25 @@ async-trait = { version = "0.1", optional = true }
## async-std ## async-std
async-std = { version = "1.8", optional = true } async-std = { version = "1.8", optional = true }
#async-native-tls = { version = "0.3.3", optional = true } #async-native-tls = { version = "0.3.3", optional = true }
futures-rustls = { version = "0.22", optional = true } futures-rustls = { version = "0.24", optional = true }
## tokio ## tokio
tokio1_crate = { package = "tokio", version = "1", optional = true } tokio1_crate = { package = "tokio", version = "1", optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true } tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.23", optional = true } tokio1_rustls = { package = "tokio-rustls", version = "0.24", optional = true }
tokio1_boring = { package = "tokio-boring", version = "2.1.4", optional = true } tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
## dkim ## dkim
sha2 = { version = "0.10", optional = true, features = ["oid"] } sha2 = { version = "0.10", optional = true, features = ["oid"] }
rsa = { version = "0.8", optional = true } rsa = { version = "0.9", optional = true }
ed25519-dalek = { version = "1.0.1", optional = true } ed25519-dalek = { version = "2", optional = true }
# email formats # email formats
email_address = { version = "0.2.1", default-features = false } email_address = { version = "0.2.1", default-features = false }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1" pretty_assertions = "1"
criterion = "0.4" criterion = "0.5"
tracing = { version = "0.1.16", default-features = false, features = ["std"] } tracing = { version = "0.1.16", default-features = false, features = ["std"] }
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
glob = "0.3" glob = "0.3"
@@ -82,39 +85,43 @@ walkdir = "2"
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] } tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] } async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1" serde_json = "1"
maud = "0.24" maud = "0.25"
[[bench]] [[bench]]
harness = false harness = false
name = "transport_smtp" name = "transport_smtp"
[[bench]]
harness = false
name = "mailbox_parsing"
[features] [features]
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"] default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"] builder = ["dep:httpdate", "dep:mime", "dep:fastrand", "dep:quoted_printable", "dep:email-encoding"]
mime03 = ["mime"] mime03 = ["dep:mime"]
# transports # transports
file-transport = ["uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"] file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "serde_json", "file-transport"] file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"] sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
smtp-transport = ["base64", "nom", "socket2", "once_cell", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"] smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:once_cell", "dep:url", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
pool = ["futures-util"] pool = ["dep:futures-util"]
rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"] rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile"]
boring-tls = ["boring"] boring-tls = ["dep:boring"]
# async # async
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"] async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"] #async-std1-native-tls = ["async-std1", "native-tls", "dep:async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "futures-rustls"] async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"] tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"] tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"] tokio1-rustls-tls = ["tokio1", "rustls-tls", "dep:tokio1_rustls"]
tokio1-boring-tls = ["tokio1", "boring-tls", "tokio1_boring"] tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
dkim = ["base64", "sha2", "rsa", "ed25519-dalek"] dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@@ -28,8 +28,8 @@
</div> </div>
<div align="center"> <div align="center">
<a href="https://deps.rs/crate/lettre/0.10.2"> <a href="https://deps.rs/crate/lettre/0.11.2">
<img src="https://deps.rs/crate/lettre/0.10.2/status.svg" <img src="https://deps.rs/crate/lettre/0.11.2/status.svg"
alt="dependency status" /> alt="dependency status" />
</a> </a>
</div> </div>
@@ -53,20 +53,21 @@ Lettre does not provide (for now):
## Supported Rust Versions ## Supported Rust Versions
Lettre supports all Rust versions released in the last 6 months. At the time of writing Lettre supports all Rust versions released in the last 6 months. At the time of writing
the minimum supported Rust version is 1.60, but this could change at any time either from the minimum supported Rust version is 1.70, but this could change at any time either from
one of our dependencies bumping their MSRV or by a new patch release of lettre. one of our dependencies bumping their MSRV or by a new patch release of lettre.
## Example ## Example
This library requires Rust 1.60 or newer. This library requires Rust 1.70 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.10" lettre = "0.11"
``` ```
```rust,no_run ```rust,no_run
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport}; use lettre::{Message, SmtpTransport, Transport};
@@ -75,10 +76,11 @@ let email = Message::builder()
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com") let mailer = SmtpTransport::relay("smtp.gmail.com")
@@ -89,7 +91,7 @@ let mailer = SmtpTransport::relay("smtp.gmail.com")
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
``` ```

View File

@@ -2,7 +2,7 @@
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues. The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
should not be reported via the public Github Issue tracker. should not be reported via the public GitHub Issue tracker.
## Security advisories ## Security advisories

View File

@@ -0,0 +1,27 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::message::{Mailbox, Mailboxes};
fn bench_parse_single(mailbox: &str) {
assert!(mailbox.parse::<Mailbox>().is_ok());
}
fn bench_parse_multiple(mailboxes: &str) {
assert!(mailboxes.parse::<Mailboxes>().is_ok());
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("parse single mailbox", |b| {
b.iter(|| bench_parse_single(black_box("\"Benchmark test\" <test@mail.local>")))
});
c.bench_function("parse multiple mailboxes", |b| {
b.iter(|| {
bench_parse_multiple(black_box(
"\"Benchmark test\" <test@mail.local>, Test <test@mail.local>, <test@mail.local>, test@mail.local",
))
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,6 +1,6 @@
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncTransport, Message, AsyncStd1Executor, AsyncTransport, Message,
}; };
#[async_std::main] #[async_std::main]
@@ -12,10 +12,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS // Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<AsyncStd1Executor> = let mailer: AsyncSmtpTransport<AsyncStd1Executor> =

View File

@@ -1,6 +1,6 @@
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncTransport, Message, AsyncStd1Executor, AsyncTransport, Message,
}; };
#[async_std::main] #[async_std::main]
@@ -12,10 +12,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer: AsyncSmtpTransport<AsyncStd1Executor> = let mailer: AsyncSmtpTransport<AsyncStd1Executor> =

View File

@@ -1,4 +1,4 @@
use lettre::{Message, SmtpTransport, Transport}; use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@@ -8,6 +8,7 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();

View File

@@ -1,6 +1,7 @@
use std::fs; use std::fs;
use lettre::{ use lettre::{
message::header::ContentType,
transport::smtp::{ transport::smtp::{
authentication::Credentials, authentication::Credentials,
client::{Certificate, Tls, TlsParameters}, client::{Certificate, Tls, TlsParameters},
@@ -16,18 +17,19 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
// Use a custom certificate stored on disk to securely verify the server's certificate // Use a custom certificate stored on disk to securely verify the server's certificate
let pem_cert = fs::read("certificate.pem").unwrap(); let pem_cert = fs::read("certificate.pem").unwrap();
let cert = Certificate::from_pem(&pem_cert).unwrap(); let cert = Certificate::from_pem(&pem_cert).unwrap();
let tls = TlsParameters::builder("smtp.server.com".to_string()) let tls = TlsParameters::builder("smtp.server.com".to_owned())
.add_root_certificate(cert) .add_root_certificate(cert)
.build() .build()
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to the smtp server // Open a remote connection to the smtp server
let mailer = SmtpTransport::builder_dangerous("smtp.server.com") let mailer = SmtpTransport::builder_dangerous("smtp.server.com")

View File

@@ -1,4 +1,7 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@@ -8,10 +11,11 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS // Open a remote connection to gmail using STARTTLS
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com") let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")

View File

@@ -1,4 +1,7 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@@ -8,10 +11,11 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com") let mailer = SmtpTransport::relay("smtp.gmail.com")

View File

@@ -2,8 +2,8 @@
// since it uses Rust 2018 crate renaming to import tokio. // since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code. // Won't be needed in user's code.
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
Tokio1Executor, AsyncTransport, Message, Tokio1Executor,
}; };
use tokio1_crate as tokio; use tokio1_crate as tokio;
@@ -16,10 +16,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS // Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<Tokio1Executor> = let mailer: AsyncSmtpTransport<Tokio1Executor> =

View File

@@ -2,8 +2,8 @@
// since it uses Rust 2018 crate renaming to import tokio. // since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code. // Won't be needed in user's code.
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
Tokio1Executor, AsyncTransport, Message, Tokio1Executor,
}; };
use tokio1_crate as tokio; use tokio1_crate as tokio;
@@ -16,10 +16,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio1Executor> = let mailer: AsyncSmtpTransport<Tokio1Executor> =

View File

@@ -15,7 +15,7 @@ use idna::domain_to_ascii;
/// ///
/// This type contains email in canonical form (_user@domain.tld_). /// This type contains email in canonical form (_user@domain.tld_).
/// ///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/). /// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
/// ///
/// # Examples /// # Examples
/// ///
@@ -227,6 +227,7 @@ fn check_address(val: &str) -> Result<usize, AddressError> {
} }
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
/// Errors in email addresses parsing /// Errors in email addresses parsing
pub enum AddressError { pub enum AddressError {
/// Missing domain or user /// Missing domain or user
@@ -237,6 +238,8 @@ pub enum AddressError {
InvalidUser, InvalidUser,
/// Invalid email domain /// Invalid email domain
InvalidDomain, InvalidDomain,
/// Invalid input found
InvalidInput,
} }
impl Error for AddressError {} impl Error for AddressError {}
@@ -248,6 +251,7 @@ impl Display for AddressError {
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"), AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
AddressError::InvalidUser => f.write_str("Invalid email user"), AddressError::InvalidUser => f.write_str("Invalid email user"),
AddressError::InvalidDomain => f.write_str("Invalid email domain"), AddressError::InvalidDomain => f.write_str("Invalid email domain"),
AddressError::InvalidInput => f.write_str("Invalid input"),
} }
} }
} }

View File

@@ -216,7 +216,7 @@ impl Executor for AsyncStd1Executor {
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep { fn sleep(duration: Duration) -> Self::Sleep {
let fut = async move { async_std::task::sleep(duration).await }; let fut = async_std::task::sleep(duration);
Box::pin(fut) Box::pin(fut)
} }

View File

@@ -6,7 +6,7 @@
//! * Secure defaults //! * Secure defaults
//! * Async support //! * Async support
//! //!
//! Lettre requires Rust 1.60 or newer. //! Lettre requires Rust 1.70 or newer.
//! //!
//! ## Features //! ## Features
//! //!
@@ -109,7 +109,7 @@
//! [mime 0.3]: https://docs.rs/mime/0.3 //! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376 //! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.2")] #![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.2")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")] #![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")] #![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -135,6 +135,8 @@
clippy::manual_assert, clippy::manual_assert,
clippy::unnecessary_join, clippy::unnecessary_join,
clippy::wildcard_imports, clippy::wildcard_imports,
clippy::str_to_string,
clippy::empty_structs_with_brackets,
clippy::zero_sized_map_values clippy::zero_sized_map_values
)] )]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
@@ -217,11 +219,11 @@ use std::error::Error as StdError;
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
pub use self::executor::AsyncStd1Executor; pub use self::executor::AsyncStd1Executor;
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::executor::Executor; pub use self::executor::Executor;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub use self::executor::Tokio1Executor; pub use self::executor::Tokio1Executor;
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[doc(inline)] #[doc(inline)]
pub use self::transport::AsyncTransport; pub use self::transport::AsyncTransport;
pub use crate::address::Address; pub use crate::address::Address;

View File

@@ -13,9 +13,9 @@ pub struct Attachment {
#[derive(Clone)] #[derive(Clone)]
enum Disposition { enum Disposition {
/// file name /// File name
Attached(String), Attached(String),
/// content id /// Content id
Inline(String), Inline(String),
} }

View File

@@ -496,7 +496,7 @@ mod test {
#[test] #[test]
fn base64_encode_bytes_wrapping() { fn base64_encode_bytes_wrapping() {
let encoded = Body::new_with_encoding( let encoded = Body::new_with_encoding(
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
ContentTransferEncoding::Base64, ContentTransferEncoding::Base64,
) )
.unwrap(); .unwrap();

View File

@@ -108,7 +108,7 @@ pub struct DkimSigningKey(InnerDkimSigningKey);
#[derive(Debug)] #[derive(Debug)]
enum InnerDkimSigningKey { enum InnerDkimSigningKey {
Rsa(RsaPrivateKey), Rsa(RsaPrivateKey),
Ed25519(ed25519_dalek::Keypair), Ed25519(ed25519_dalek::SigningKey),
} }
impl DkimSigningKey { impl DkimSigningKey {
@@ -121,14 +121,18 @@ impl DkimSigningKey {
RsaPrivateKey::from_pkcs1_pem(private_key) RsaPrivateKey::from_pkcs1_pem(private_key)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?, .map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
), ),
DkimSigningAlgorithm::Ed25519 => InnerDkimSigningKey::Ed25519( DkimSigningAlgorithm::Ed25519 => {
ed25519_dalek::Keypair::from_bytes( InnerDkimSigningKey::Ed25519(ed25519_dalek::SigningKey::from_bytes(
&crate::base64::decode(private_key).map_err(|err| { &crate::base64::decode(private_key)
DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)) .map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)))?
})?, .try_into()
) .map_err(|_| {
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?, DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(
), ed25519_dalek::ed25519::Error::new(),
))
})?,
))
}
})) }))
} }
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm { fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
@@ -140,19 +144,18 @@ impl DkimSigningKey {
} }
/// A struct to describe Dkim configuration applied when signing a message /// A struct to describe Dkim configuration applied when signing a message
/// selector: the name of the key publied in DNS
/// domain: the domain for which we sign the message
/// private_key: private key in PKCS1 string format
/// headers: a list of headers name to be included in the signature. Signing of more than one
/// header with same name is not supported
/// canonicalization: the canonicalization to be applied on the message
/// pub signing_algorithm: the signing algorithm to be used when signing
#[derive(Debug)] #[derive(Debug)]
pub struct DkimConfig { pub struct DkimConfig {
/// The name of the key published in DNS
selector: String, selector: String,
/// The domain for which we sign the message
domain: String, domain: String,
/// The private key in PKCS1 string format
private_key: DkimSigningKey, private_key: DkimSigningKey,
/// A list of header names to be included in the signature. Signing of more than one
/// header with the same name is not supported
headers: Vec<HeaderName>, headers: Vec<HeaderName>,
/// The signing algorithm to be used when signing
canonicalization: DkimCanonicalization, canonicalization: DkimCanonicalization,
} }
@@ -341,7 +344,7 @@ fn dkim_canonicalize_headers<'a>(
} }
} }
/// Sign with Dkim a message by adding Dkim-Signture header created with configuration expressed by /// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
/// dkim_config /// dkim_config
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) { pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
@@ -477,9 +480,9 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
.from("Test O'Leary <test+ezrz@example.net>".parse().unwrap()) .from("Test O'Leary <test+ezrz@example.net>".parse().unwrap())
.to("Test2 <test2@example.org>".parse().unwrap()) .to("Test2 <test2@example.org>".parse().unwrap())
.date(std::time::UNIX_EPOCH) .date(std::time::UNIX_EPOCH)
.header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_string())) .header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_owned()))
.subject("Test with utf-8 ë") .subject("Test with utf-8 ë")
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_string()).unwrap() .body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_owned()).unwrap()
} }
#[test] #[test]
@@ -521,8 +524,8 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
dkim_sign_fixed_time( dkim_sign_fixed_time(
&mut message, &mut message,
&DkimConfig::new( &DkimConfig::new(
"dkimtest".to_string(), "dkimtest".to_owned(),
"example.org".to_string(), "example.org".to_owned(),
signing_key, signing_key,
vec![ vec![
HeaderName::new_from_ascii_str("Date"), HeaderName::new_from_ascii_str("Date"),
@@ -570,8 +573,8 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
dkim_sign_fixed_time( dkim_sign_fixed_time(
&mut message, &mut message,
&DkimConfig::new( &DkimConfig::new(
"dkimtest".to_string(), "dkimtest".to_owned(),
"example.org".to_string(), "example.org".to_owned(),
signing_key, signing_key,
vec![ vec![
HeaderName::new_from_ascii_str("Date"), HeaderName::new_from_ascii_str("Date"),

View File

@@ -13,12 +13,14 @@ use crate::BoxError;
/// use-caches this header shouldn't be set manually. /// use-caches this header shouldn't be set manually.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default)]
pub enum ContentTransferEncoding { pub enum ContentTransferEncoding {
/// ASCII /// ASCII
SevenBit, SevenBit,
/// Quoted-Printable encoding /// Quoted-Printable encoding
QuotedPrintable, QuotedPrintable,
/// base64 encoding /// base64 encoding
#[default]
Base64, Base64,
/// Requires `8BITMIME` /// Requires `8BITMIME`
EightBit, EightBit,
@@ -67,12 +69,6 @@ impl FromStr for ContentTransferEncoding {
} }
} }
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::Base64
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -99,7 +95,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"7bit".to_string(), "7bit".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -109,7 +105,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"base64".to_string(), "base64".to_owned(),
)); ));
assert_eq!( assert_eq!(

View File

@@ -16,13 +16,13 @@ impl ContentDisposition {
pub fn inline() -> Self { pub fn inline() -> Self {
Self(HeaderValue::dangerous_new_pre_encoded( Self(HeaderValue::dangerous_new_pre_encoded(
Self::name(), Self::name(),
"inline".to_string(), "inline".to_owned(),
"inline".to_string(), "inline".to_owned(),
)) ))
} }
/// An attachment which should be displayed inline into the message, but that also /// An attachment which should be displayed inline into the message, but that also
/// species the filename in case it were to be downloaded /// species the filename in case it is downloaded
pub fn inline_with_name(file_name: &str) -> Self { pub fn inline_with_name(file_name: &str) -> Self {
Self::with_name("inline", file_name) Self::with_name("inline", file_name)
} }
@@ -106,7 +106,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"), HeaderName::new_from_ascii_str("Content-Disposition"),
"inline".to_string(), "inline".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -116,7 +116,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"), HeaderName::new_from_ascii_str("Content-Disposition"),
"attachment; filename=\"something.txt\"".to_string(), "attachment; filename=\"something.txt\"".to_owned(),
)); ));
assert_eq!( assert_eq!(

View File

@@ -11,12 +11,12 @@ use crate::BoxError;
/// `Content-Type` of the body /// `Content-Type` of the body
/// ///
/// This struct can represent any valid [mime type], which can be parsed via /// This struct can represent any valid [MIME type], which can be parsed via
/// [`ContentType::parse`]. Constants are provided for the most-used mime-types. /// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
/// ///
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5) /// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
/// ///
/// [mime type]: https://www.iana.org/assignments/media-types/media-types.xhtml /// [MIME type]: https://www.iana.org/assignments/media-types/media-types.xhtml
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentType(Mime); pub struct ContentType(Mime);
@@ -178,14 +178,14 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"), HeaderName::new_from_ascii_str("Content-Type"),
"text/plain; charset=utf-8".to_string(), "text/plain; charset=utf-8".to_owned(),
)); ));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN)); assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"), HeaderName::new_from_ascii_str("Content-Type"),
"text/html; charset=utf-8".to_string(), "text/html; charset=utf-8".to_owned(),
)); ));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML)); assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));

View File

@@ -90,7 +90,7 @@ mod test {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_string() "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_owned()
); );
// Tue, 15 Nov 1994 08:12:32 GMT // Tue, 15 Nov 1994 08:12:32 GMT
@@ -110,7 +110,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"), HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:31 +0000".to_string(), "Tue, 15 Nov 1994 08:12:31 +0000".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -122,7 +122,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"), HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:32 +0000".to_string(), "Tue, 15 Nov 1994 08:12:32 +0000".to_owned(),
)); ));
assert_eq!( assert_eq!(

View File

@@ -252,7 +252,7 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"kayo@example.com".to_string(), "kayo@example.com".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from))); assert_eq!(headers.get::<From>(), Some(From(from)));
@@ -265,7 +265,7 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>".to_string(), "K. <kayo@example.com>".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from))); assert_eq!(headers.get::<From>(), Some(From(from)));
@@ -281,7 +281,7 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"kayo@example.com, pony@domain.tld".to_string(), "kayo@example.com, pony@domain.tld".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -297,7 +297,7 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(), "K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -306,14 +306,30 @@ mod test {
#[test] #[test]
fn parse_multi_with_name_containing_comma() { fn parse_multi_with_name_containing_comma() {
let from: Vec<Mailbox> = vec![ let from: Vec<Mailbox> = vec![
"Test, test <1@example.com>".parse().unwrap(), "\"Test, test\" <1@example.com>".parse().unwrap(),
"Test2, test2 <2@example.com>".parse().unwrap(), "\"Test2, test2\" <2@example.com>".parse().unwrap(),
]; ];
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2 <2@example.com>".to_string(), "\"Test, test\" <1@example.com>, \"Test2, test2\" <2@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
fn parse_multi_with_name_containing_double_quotes() {
let from: Vec<Mailbox> = vec![
"\"Test, test\" <1@example.com>".parse().unwrap(),
"\"Test2, \"test2\"\" <2@example.com>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"\"Test, test\" <1@example.com>, \"Test2, \"test2\"\" <2@example.com>".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -324,9 +340,20 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2".to_string(), "\"Test, test\" <1@example.com>, \"Test2, test2\"".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), None); assert_eq!(headers.get::<From>(), None);
} }
#[test]
fn mailbox_format_address_with_angle_bracket() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("<3".into()), "i@love.example".parse().unwrap())
),
r#""<3" <i@love.example>"#
);
}
} }

View File

@@ -66,7 +66,7 @@ impl Headers {
} }
} }
/// Returns a copy of an `Header` present in `Headers` /// Returns a copy of a `Header` present in `Headers`
/// ///
/// Returns `None` if `Header` isn't present in `Headers`. /// Returns `None` if `Header` isn't present in `Headers`.
pub fn get<H: Header>(&self) -> Option<H> { pub fn get<H: Header>(&self) -> Option<H> {
@@ -310,7 +310,7 @@ impl HeaderValue {
/// acceptable for use if `encoded_value` contains only ascii /// acceptable for use if `encoded_value` contains only ascii
/// printable characters and is already line folded. /// printable characters and is already line folded.
/// ///
/// When in doubt use [`HeaderValue::new`]. /// When in doubt, use [`HeaderValue::new`].
pub fn dangerous_new_pre_encoded( pub fn dangerous_new_pre_encoded(
name: HeaderName, name: HeaderName,
raw_value: String, raw_value: String,
@@ -474,7 +474,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_string(), "John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -488,7 +488,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_string(), "Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -506,7 +506,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string() "Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
)); ));
assert_eq!( assert_eq!(
@@ -525,7 +525,7 @@ mod tests {
headers.insert_raw( headers.insert_raw(
HeaderValue::new( HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string() "Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_owned()
)); ));
assert_eq!( assert_eq!(
@@ -543,7 +543,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string() "1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_owned()
)); ));
assert_eq!( assert_eq!(
@@ -557,7 +557,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"Seán <sean@example.com>".to_string(), "Seán <sean@example.com>".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -571,7 +571,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"🌎 <world@example.com>".to_string(), "🌎 <world@example.com>".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -609,7 +609,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(), "🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
)); ));
// TODO: fix the fact that the encoder doesn't know that // TODO: fix the fact that the encoder doesn't know that
@@ -633,7 +633,7 @@ mod tests {
headers.insert_raw( headers.insert_raw(
HeaderValue::new( HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(),) "🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_owned(),)
); );
assert_eq!( assert_eq!(
@@ -654,7 +654,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! \r\n This is \" bad \0. 👋".to_string(), "Hello! \r\n This is \" bad \0. 👋".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -669,22 +669,22 @@ mod tests {
headers.insert_raw( headers.insert_raw(
HeaderValue::new( HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string() "Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
) )
); );
headers.insert_raw( headers.insert_raw(
HeaderValue::new( HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(), "🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
) )
); );
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"Someone <somewhere@example.com>".to_string(), "Someone <somewhere@example.com>".to_owned(),
)); ));
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"quoted-printable".to_string(), "quoted-printable".to_owned(),
)); ));
// TODO: fix the fact that the encoder doesn't know that // TODO: fix the fact that the encoder doesn't know that
@@ -712,7 +712,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg".to_string(), "+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg".to_owned(),
)); ));
assert_eq!( assert_eq!(

View File

@@ -91,14 +91,14 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"), HeaderName::new_from_ascii_str("MIME-Version"),
"1.0".to_string(), "1.0".to_owned(),
)); ));
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0)); assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"), HeaderName::new_from_ascii_str("MIME-Version"),
"0.1".to_string(), "0.1".to_owned(),
)); ));
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1))); assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));

View File

@@ -125,7 +125,7 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Sample subject".to_string(), "Sample subject".to_owned(),
)); ));
assert_eq!( assert_eq!(

View File

@@ -1,3 +1,4 @@
mod parsers;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
mod serde; mod serde;
mod types; mod types;

View File

@@ -0,0 +1,5 @@
mod rfc2234;
mod rfc2822;
mod rfc5336;
pub(crate) use rfc2822::{mailbox, mailbox_list};

View File

@@ -0,0 +1,32 @@
//! Partial parsers implementation of [RFC2234]: Augmented BNF for
//! Syntax Specifications: ABNF.
//!
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
use chumsky::{error::Cheap, prelude::*};
// 6.1 Core Rules
// https://datatracker.ietf.org/doc/html/rfc2234#section-6.1
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.is_ascii_alphabetic())
}
// DIGIT = %x30-39
// ; 0-9
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.is_ascii_digit())
}
// DQUOTE = %x22
// ; " (Double Quote)
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
just('"')
}
// WSP = SP / HTAB
// ; white space
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
choice((just(' '), just('\t')))
}

View File

@@ -0,0 +1,248 @@
//! Partial parsers implementation of [RFC2822]: Internet Message
//! Format.
//!
//! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822
use chumsky::{error::Cheap, prelude::*};
use super::{rfc2234, rfc5336};
// 3.2.1. Primitive Tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
// NO-WS-CTL = %d1-8 / ; US-ASCII control characters
// %d11 / ; that do not include the
// %d12 / ; carriage return, line feed,
// %d14-31 / ; and white space characters
// %d127
fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c| matches!(u32::from(*c), 1..=8 | 11 | 12 | 14..=31 | 127))
}
// text = %d1-9 / ; Characters excluding CR and LF
// %d11 /
// %d12 /
// %d14-127 /
// obs-text
fn text() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c| matches!(u32::from(*c), 1..=9 | 11 | 12 | 14..=127))
}
// 3.2.2. Quoted characters
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
// quoted-pair = ("\" text) / obs-qp
fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
just('\\').ignore_then(text())
}
// 3.2.3. Folding white space and comments
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.3
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
// obs-FWS
pub fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
rfc2234::wsp()
.or_not()
.then_ignore(rfc2234::wsp().ignored().repeated())
}
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
// TODO: comment are not currently supported, so for now a cfws is
// the same as a fws.
fws()
}
// 3.2.4. Atom
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4
// atext = ALPHA / DIGIT / ; Any character except controls,
// "!" / "#" / ; SP, and specials.
// "$" / "%" / ; Used for atoms
// "&" / "'" /
// "*" / "+" /
// "-" / "/" /
// "=" / "?" /
// "^" / "_" /
// "`" / "{" /
// "|" / "}" /
// "~"
pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
choice((
rfc2234::alpha(),
rfc2234::digit(),
filter(|c| {
matches!(
*c,
'!' | '#'
| '$'
| '%'
| '&'
| '\''
| '*'
| '+'
| '-'
| '/'
| '='
| '?'
| '^'
| '_'
| '`'
| '{'
| '|'
| '}'
| '~'
)
}),
// also allow non ASCII UTF8 chars
rfc5336::utf8_non_ascii(),
))
}
// atom = [CFWS] 1*atext [CFWS]
pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(atext().repeated().at_least(1))
}
// dot-atom = [CFWS] dot-atom-text [CFWS]
pub fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(dot_atom_text())
}
// dot-atom-text = 1*atext *("." 1*atext)
pub fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atext().repeated().at_least(1).chain(
just('.')
.chain(atext().repeated().at_least(1))
.repeated()
.at_least(1)
.flatten(),
)
}
// 3.2.5. Quoted strings
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
// qtext = NO-WS-CTL / ; Non white space controls
//
// %d33 / ; The rest of the US-ASCII
// %d35-91 / ; characters not including "\"
// %d93-126 ; or the quote character
fn qtext() -> impl Parser<char, char, Error = Cheap<char>> {
choice((
filter(|c| matches!(u32::from(*c), 33 | 35..=91 | 93..=126)),
no_ws_ctl(),
))
}
// qcontent = qtext / quoted-pair
pub(super) fn qcontent() -> impl Parser<char, char, Error = Cheap<char>> {
choice((qtext(), quoted_pair(), rfc5336::utf8_non_ascii()))
}
// quoted-string = [CFWS]
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
// [CFWS]
fn quoted_string() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
rfc2234::dquote()
.ignore_then(fws().chain(qcontent()).repeated().flatten())
.then_ignore(text::whitespace())
.then_ignore(rfc2234::dquote())
}
// 3.2.6. Miscellaneous tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
// word = atom / quoted-string
fn word() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((quoted_string(), atom()))
}
// phrase = 1*word / obs-phrase
fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((obs_phrase(), word().repeated().at_least(1).flatten()))
}
// 3.4. Address Specification
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
// mailbox = name-addr / addr-spec
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>>
{
choice((name_addr(), addr_spec().map(|addr| (None, addr)))).then_ignore(end())
}
// name-addr = [display-name] angle-addr
fn name_addr() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> {
display_name().collect().or_not().then(angle_addr())
}
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
fn angle_addr() -> impl Parser<char, (String, String), Error = Cheap<char>> {
addr_spec()
.delimited_by(just('<').ignored(), just('>').ignored())
.padded()
}
// display-name = phrase
fn display_name() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
phrase()
}
// mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
pub(crate) fn mailbox_list(
) -> impl Parser<char, Vec<(Option<String>, (String, String))>, Error = Cheap<char>> {
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
.separated_by(just(',').padded())
.then_ignore(end())
}
// 3.4.1. Addr-spec specification
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
// addr-spec = local-part "@" domain
pub fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
local_part()
.collect()
.then_ignore(just('@'))
.then(domain().collect())
}
// local-part = dot-atom / quoted-string / obs-local-part
pub fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((dot_atom(), quoted_string(), obs_local_part()))
}
// domain = dot-atom / domain-literal / obs-domain
pub fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// NOTE: omitting domain-literal since it may never be used
choice((dot_atom(), obs_domain()))
}
// 4.1. Miscellaneous obsolete tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.1
// obs-phrase = word *(word / "." / CFWS)
fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// NOTE: the CFWS is already captured by the word, no need to add
// it there.
word().chain(
choice((word(), just('.').repeated().exactly(1)))
.repeated()
.flatten(),
)
}
// 4.4. Obsolete Addressing
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
// obs-local-part = word *("." word)
pub fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
word().chain(just('.').chain(word()).repeated().flatten())
}
// obs-domain = atom *("." atom)
pub fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atom().chain(just('.').chain(atom()).repeated().flatten())
}

View File

@@ -0,0 +1,17 @@
//! Partial parsers implementation of [RFC5336]: SMTP Extension for
//! Internationalized Email Addresses.
//!
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
use chumsky::{error::Cheap, prelude::*};
// 3.3. Extended Mailbox Address Syntax
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
// UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
// UTF8-2 = <See Section 4 of RFC 3629>
// UTF8-3 = <See Section 4 of RFC 3629>
// UTF8-4 = <See Section 4 of RFC 3629>
pub(super) fn utf8_non_ascii() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.len_utf8() > 1)
}

View File

@@ -13,7 +13,7 @@ impl Serialize for Mailbox {
where where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(&self.to_string()) serializer.collect_str(self)
} }
} }
@@ -111,7 +111,7 @@ impl Serialize for Mailboxes {
where where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(&self.to_string()) serializer.collect_str(self)
} }
} }
@@ -179,7 +179,7 @@ mod test {
} }
#[test] #[test]
fn parse_mailbox_object_address_stirng() { fn parse_mailbox_object_address_string() {
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap(); let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap()); assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
} }
@@ -198,7 +198,7 @@ mod test {
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap(); from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
assert_eq!( assert_eq!(
m, m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>" "yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse() .parse()
.unwrap() .unwrap()
); );
@@ -211,7 +211,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
m, m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>" "yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse() .parse()
.unwrap() .unwrap()
); );

View File

@@ -5,15 +5,17 @@ use std::{
str::FromStr, str::FromStr,
}; };
use chumsky::prelude::*;
use email_encoding::headers::EmailWriter; use email_encoding::headers::EmailWriter;
use super::parsers;
use crate::address::{Address, AddressError}; use crate::address::{Address, AddressError};
/// Represents an email address with an optional name for the sender/recipient. /// Represents an email address with an optional name for the sender/recipient.
/// ///
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_). /// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
/// ///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/). /// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
/// ///
/// # Examples /// # Examples
/// ///
@@ -108,40 +110,24 @@ impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
} }
} }
/*
impl<S: AsRef<&str>, T: AsRef<&str>> TryFrom<(S, T)> for Mailbox {
type Error = AddressError;
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
let (name, address) = header;
Ok(Mailbox::new(Some(name.as_ref()), address.as_ref().parse()?))
}
}*/
impl FromStr for Mailbox { impl FromStr for Mailbox {
type Err = AddressError; type Err = AddressError;
fn from_str(src: &str) -> Result<Mailbox, Self::Err> { fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
match (src.find('<'), src.find('>')) { let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => { // TODO: improve error management
let name = src.split_at(addr_open).0; AddressError::InvalidInput
let addr_open = addr_open + 1; })?;
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
let addr = addr.parse()?; let mailbox = Mailbox::new(name, Address::new(user, domain)?);
let name = name.trim();
let name = if name.is_empty() { Ok(mailbox)
None }
} else { }
Some(name.into())
}; impl From<Address> for Mailbox {
Ok(Mailbox::new(name, addr)) fn from(value: Address) -> Self {
} Self::new(None, value)
(Some(_), _) => Err(AddressError::Unbalanced),
_ => {
let addr = src.parse()?;
Ok(Mailbox::new(None, addr))
}
}
} }
} }
@@ -149,7 +135,7 @@ impl FromStr for Mailbox {
/// ///
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._). /// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
/// ///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/). /// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailboxes(Vec<Mailbox>); pub struct Mailboxes(Vec<Mailbox>);
@@ -356,34 +342,16 @@ impl Display for Mailboxes {
impl FromStr for Mailboxes { impl FromStr for Mailboxes {
type Err = AddressError; type Err = AddressError;
fn from_str(mut src: &str) -> Result<Self, Self::Err> { fn from_str(src: &str) -> Result<Self, Self::Err> {
let mut mailboxes = Vec::new(); let mut mailboxes = Vec::new();
if !src.is_empty() { let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
// n-1 elements // TODO: improve error management
let mut skip = 0; AddressError::InvalidInput
while let Some(i) = src[skip..].find(',') { })?;
let left = &src[..skip + i];
match left.trim().parse() { for (name, (user, domain)) in parsed_mailboxes {
Ok(mailbox) => { mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
mailboxes.push(mailbox);
src = &src[left.len() + ",".len()..];
skip = 0;
}
Err(AddressError::MissingParts) => {
skip = left.len() + ",".len();
}
Err(err) => {
return Err(err);
}
}
}
// last element
let mailbox = src.trim().parse()?;
mailboxes.push(mailbox);
} }
Ok(Mailboxes(mailboxes)) Ok(Mailboxes(mailboxes))
@@ -436,7 +404,7 @@ fn is_valid_atom_char(c: u8) -> bool {
b'}' | b'}' |
b'~' | b'~' |
// Not techically allowed but will be escaped into allowed characters. // Not technically allowed but will be escaped into allowed characters.
128..=255) 128..=255)
} }
@@ -620,7 +588,7 @@ mod test {
#[test] #[test]
fn parse_address_from_tuple() { fn parse_address_from_tuple() {
assert_eq!( assert_eq!(
("K.".to_string(), "kayo@example.com".to_string()).try_into(), ("K.".to_owned(), "kayo@example.com".to_owned()).try_into(),
Ok(Mailbox::new( Ok(Mailbox::new(
Some("K.".into()), Some("K.".into()),
"kayo@example.com".parse().unwrap() "kayo@example.com".parse().unwrap()

View File

@@ -100,14 +100,14 @@ impl SinglePart {
SinglePartBuilder::new() SinglePartBuilder::new()
} }
/// Directly create a `SinglePart` from an plain UTF-8 content /// Directly create a `SinglePart` from a plain UTF-8 content
pub fn plain<T: IntoBody>(body: T) -> Self { pub fn plain<T: IntoBody>(body: T) -> Self {
Self::builder() Self::builder()
.header(header::ContentType::TEXT_PLAIN) .header(header::ContentType::TEXT_PLAIN)
.body(body) .body(body)
} }
/// Directly create a `SinglePart` from an UTF-8 HTML content /// Directly create a `SinglePart` from a UTF-8 HTML content
pub fn html<T: IntoBody>(body: T) -> Self { pub fn html<T: IntoBody>(body: T) -> Self {
Self::builder() Self::builder()
.header(header::ContentType::TEXT_HTML) .header(header::ContentType::TEXT_HTML)
@@ -149,17 +149,17 @@ impl EmailFormat for SinglePart {
pub enum MultiPartKind { pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts /// Mixed kind to combine unrelated content parts
/// ///
/// For example this kind can be used to mix email message and attachments. /// For example, this kind can be used to mix an email message and attachments.
Mixed, Mixed,
/// Alternative kind to join several variants of same email contents. /// Alternative kind to join several variants of same email contents.
/// ///
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message. /// That kind is recommended to use for joining plain (text) and rich (HTML) messages into a single email message.
Alternative, Alternative,
/// Related kind to mix content and related resources. /// Related kind to mix content and related resources.
/// ///
/// For example, you can include images into HTML content using that. /// For example, you can include images in HTML content using that.
Related, Related,
/// Encrypted kind for encrypted messages /// Encrypted kind for encrypted messages

View File

@@ -345,9 +345,9 @@ impl MessageBuilder {
let hostname = hostname::get() let hostname = hostname::get()
.map_err(|_| ()) .map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ())) .and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string()); .unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
#[cfg(not(feature = "hostname"))] #[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string(); let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
self.header(header::MessageId::from( self.header(header::MessageId::from(
// https://tools.ietf.org/html/rfc5322#section-3.6.4 // https://tools.ietf.org/html/rfc5322#section-3.6.4
@@ -388,13 +388,13 @@ impl MessageBuilder {
/// Keep the `Bcc` header /// Keep the `Bcc` header
/// ///
/// By default the `Bcc` header is removed from the email after /// By default, the `Bcc` header is removed from the email after
/// using it to generate the message envelope. In some cases though, /// using it to generate the message envelope. In some cases though,
/// like when saving the email as an `.eml`, or sending through /// like when saving the email as an `.eml`, or sending through
/// some transports (like the Gmail API) that don't take a separate /// some transports (like the Gmail API) that don't take a separate
/// envelope value, it becomes necessary to keep the `Bcc` header. /// envelope value, it becomes necessary to keep the `Bcc` header.
/// ///
/// Calling this method overrides the default behaviour. /// Calling this method overrides the default behavior.
pub fn keep_bcc(mut self) -> Self { pub fn keep_bcc(mut self) -> Self {
self.drop_bcc = false; self.drop_bcc = false;
self self
@@ -503,6 +503,11 @@ impl Message {
&self.headers &self.headers
} }
/// Get a mutable reference to the headers
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}
/// Get `Message` envelope /// Get `Message` envelope
pub fn envelope(&self) -> &Envelope { pub fn envelope(&self) -> &Envelope {
&self.envelope &self.envelope
@@ -541,7 +546,7 @@ impl Message {
/// .reply_to("Bob <bob@example.org>".parse().unwrap()) /// .reply_to("Bob <bob@example.org>".parse().unwrap())
/// .to("Carla <carla@example.net>".parse().unwrap()) /// .to("Carla <carla@example.net>".parse().unwrap())
/// .subject("Hello") /// .subject("Hello")
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_string()) /// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
/// .unwrap(); /// .unwrap();
/// let key = "-----BEGIN RSA PRIVATE KEY----- /// let key = "-----BEGIN RSA PRIVATE KEY-----
/// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8 /// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
@@ -572,8 +577,8 @@ impl Message {
/// -----END RSA PRIVATE KEY-----"; /// -----END RSA PRIVATE KEY-----";
/// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap(); /// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
/// message.sign(&DkimConfig::default_config( /// message.sign(&DkimConfig::default_config(
/// "dkimtest".to_string(), /// "dkimtest".to_owned(),
/// "example.org".to_string(), /// "example.org".to_owned(),
/// signing_key, /// signing_key,
/// )); /// ));
/// println!( /// println!(
@@ -630,7 +635,7 @@ mod test {
} }
#[test] #[test]
fn email_miminal_message() { fn email_minimal_message() {
assert!(Message::builder() assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("NoBody <nobody@domain.tld>".parse().unwrap()) .to("NoBody <nobody@domain.tld>".parse().unwrap())

View File

@@ -65,7 +65,7 @@
//! .subject("Happy new year") //! .subject("Happy new year")
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); //! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
//! //!
//! // Open a remote connection to the SMTP relay server //! // Open a remote connection to the SMTP relay server
//! let mailer = SmtpTransport::relay("smtp.gmail.com")? //! let mailer = SmtpTransport::relay("smtp.gmail.com")?
@@ -75,7 +75,7 @@
//! // Send the email //! // Send the email
//! match mailer.send(&email) { //! match mailer.send(&email) {
//! Ok(_) => println!("Email sent successfully!"), //! Ok(_) => println!("Email sent successfully!"),
//! Err(e) => panic!("Could not send email: {:?}", e), //! Err(e) => panic!("Could not send email: {e:?}"),
//! } //! }
//! # Ok(()) //! # Ok(())
//! # } //! # }

View File

@@ -103,7 +103,7 @@ where
.tls(Tls::Wrapper(tls_parameters))) .tls(Tls::Wrapper(tls_parameters)))
} }
/// Simple an secure transport, using STARTTLS to obtain encrypted connections /// Simple and secure transport, using STARTTLS to obtain encrypted connections
/// ///
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers /// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections. /// that don't take SMTPS connections.
@@ -151,28 +151,114 @@ where
/// ///
/// * No authentication /// * No authentication
/// * No TLS /// * No TLS
/// * A 60 seconds timeout for smtp commands /// * A 60-seconds timeout for smtp commands
/// * Port 25 /// * Port 25
/// ///
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or /// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead, /// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible. /// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder { pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
let info = SmtpInfo { AsyncSmtpTransportBuilder::new(server)
server: server.into(), }
..Default::default()
}; /// Creates a `AsyncSmtpTransportBuilder` from a connection URL
AsyncSmtpTransportBuilder { ///
info, /// The protocol, credentials, host and port can be provided in a single URL.
#[cfg(feature = "pool")] /// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
pool_config: PoolConfig::default(), /// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
} /// The path section of the url can be used to set an alternative name for
/// the HELO / EHLO command.
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
/// will set the HELO / EHLO name `client.example.com`.
///
/// <table>
/// <thead>
/// <tr>
/// <th>scheme</th>
/// <th>tls parameter</th>
/// <th>example</th>
/// <th>remarks</th>
/// </tr>
/// </thead>
/// <tbody>
/// <tr>
/// <td>smtps</td>
/// <td>-</td>
/// <td>smtps://smtp.example.com</td>
/// <td>SMTP over TLS, recommended method</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>required</td>
/// <td>smtp://smtp.example.com?tls=required</td>
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>opportunistic</td>
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
/// <td>
/// SMTP with optionally STARTTLS when supported by the server.
/// Caution: this method is vulnerable to a man-in-the-middle attack.
/// Not recommended for production use.
/// </td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>-</td>
/// <td>smtp://smtp.example.com</td>
/// <td>Unencrypted SMTP, not recommended for production use.</td>
/// </tr>
/// </tbody>
/// </table>
///
/// ```rust,no_run
/// use lettre::{
/// message::header::ContentType, transport::smtp::authentication::Credentials,
/// AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
/// };
/// # use tokio1_crate as tokio;
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let email = Message::builder()
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
/// .to("Hei <hei@domain.tld>".parse().unwrap())
/// .subject("Happy new year")
/// .header(ContentType::TEXT_PLAIN)
/// .body(String::from("Be happy!"))
/// .unwrap();
///
/// // Open a remote connection to gmail
/// let mailer: AsyncSmtpTransport<Tokio1Executor> =
/// AsyncSmtpTransport::<Tokio1Executor>::from_url(
/// "smtps://username:password@smtp.example.com:465",
/// )
/// .unwrap()
/// .build();
///
/// // Send the email
/// match mailer.send(email).await {
/// Ok(_) => println!("Email sent successfully!"),
/// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// # Ok(())
/// # }
/// ```
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn from_url(connection_url: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
super::connection_url::from_connection_url(connection_url)
} }
/// Tests the SMTP connection /// Tests the SMTP connection
/// ///
/// `test_connection()` tests the connection by using the SMTP NOOP command. /// `test_connection()` tests the connection by using the SMTP NOOP command.
/// The connection is closed afterwards if a connection pool is not used. /// The connection is closed afterward if a connection pool is not used.
pub async fn test_connection(&self) -> Result<bool, Error> { pub async fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection().await?; let mut conn = self.inner.connection().await?;
@@ -219,6 +305,20 @@ pub struct AsyncSmtpTransportBuilder {
/// Builder for the SMTP `AsyncSmtpTransport` /// Builder for the SMTP `AsyncSmtpTransport`
impl AsyncSmtpTransportBuilder { impl AsyncSmtpTransportBuilder {
// Create new builder with default parameters
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
let info = SmtpInfo {
server: server.into(),
..Default::default()
};
AsyncSmtpTransportBuilder {
info,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Set the name used during EHLO /// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self { pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name; self.info.hello_name = name;

View File

@@ -51,7 +51,7 @@ pub enum Mechanism {
/// [RFC 4616](https://tools.ietf.org/html/rfc4616) /// [RFC 4616](https://tools.ietf.org/html/rfc4616)
Plain, Plain,
/// LOGIN authentication mechanism /// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like office365) /// Obsolete but needed for some providers (like Office 365)
/// ///
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt). /// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
Login, Login,
@@ -71,7 +71,7 @@ impl Display for Mechanism {
} }
impl Mechanism { impl Mechanism {
/// Does the mechanism supports initial response /// Does the mechanism support initial response?
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 | Mechanism::Xoauth2 => true,
@@ -127,7 +127,7 @@ mod test {
fn test_plain() { fn test_plain() {
let mechanism = Mechanism::Plain; let mechanism = Mechanism::Plain;
let credentials = Credentials::new("username".to_string(), "password".to_string()); let credentials = Credentials::new("username".to_owned(), "password".to_owned());
assert_eq!( assert_eq!(
mechanism.response(&credentials, None).unwrap(), mechanism.response(&credentials, None).unwrap(),
@@ -140,7 +140,7 @@ mod test {
fn test_login() { fn test_login() {
let mechanism = Mechanism::Login; let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string()); let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
assert_eq!( assert_eq!(
mechanism.response(&credentials, Some("Username")).unwrap(), mechanism.response(&credentials, Some("Username")).unwrap(),
@@ -158,8 +158,8 @@ mod test {
let mechanism = Mechanism::Xoauth2; let mechanism = Mechanism::Xoauth2;
let credentials = Credentials::new( let credentials = Credentials::new(
"username".to_string(), "username".to_owned(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(), "vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
); );
assert_eq!( assert_eq!(
@@ -172,7 +172,7 @@ mod test {
#[test] #[test]
fn test_from_user_pass_for_credentials() { fn test_from_user_pass_for_credentials() {
assert_eq!( assert_eq!(
Credentials::new("alice".to_string(), "wonderland".to_string()), Credentials::new("alice".to_owned(), "wonderland".to_owned()),
Credentials::from(("alice", "wonderland")) Credentials::from(("alice", "wonderland"))
); );
} }

View File

@@ -62,7 +62,35 @@ impl AsyncSmtpConnection {
/// Connects to the configured server /// Connects to the configured server
/// ///
/// If `tls_parameters` is `Some`, then the connection will use Implicit TLS (sometimes
/// referred to as `SMTPS`). See also [`AsyncSmtpConnection::starttls`].
///
/// If `local_addres` is `Some`, then the address provided shall be used to bind the
/// connection to a specific local address using [`tokio1_crate::net::TcpSocket::bind`].
///
/// Sends EHLO and parses server information /// Sends EHLO and parses server information
///
/// # Example
///
/// ```no_run
/// # use std::time::Duration;
/// # use lettre::transport::smtp::{client::{AsyncSmtpConnection, TlsParameters}, extension::ClientId};
/// # use tokio1_crate::{self as tokio, net::ToSocketAddrs as _};
/// #
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let connection = AsyncSmtpConnection::connect_tokio1(
/// ("example.com", 465),
/// Some(Duration::from_secs(60)),
/// &ClientId::default(),
/// Some(TlsParameters::new("example.com".to_owned())?),
/// None,
/// )
/// .await
/// .unwrap();
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>( pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
server: T, server: T,
@@ -132,7 +160,7 @@ impl AsyncSmtpConnection {
mail_options.push(MailParameter::SmtpUtfEight); mail_options.push(MailParameter::SmtpUtfEight);
} }
// Check for non-ascii content in message // Check for non-ascii content in the message
if !email.is_ascii() { if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) { if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(error::client( return Err(error::client(
@@ -172,6 +200,12 @@ impl AsyncSmtpConnection {
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls) !self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
} }
/// Upgrade the connection using `STARTTLS`.
///
/// As described in [rfc3207]. Note that this mechanism has been deprecated in [rfc8314].
///
/// [rfc3207]: https://www.rfc-editor.org/rfc/rfc3207
/// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314
#[allow(unused_variables)] #[allow(unused_variables)]
pub async fn starttls( pub async fn starttls(
&mut self, &mut self,
@@ -226,7 +260,7 @@ impl AsyncSmtpConnection {
self.command(Noop).await.is_ok() self.command(Noop).await.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 the challenge if needed
pub async fn auth( pub async fn auth(
&mut self, &mut self,
mechanisms: &[Mechanism], mechanisms: &[Mechanism],

View File

@@ -208,9 +208,9 @@ impl AsyncNetworkStream {
timeout: Option<Duration>, timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> { ) -> Result<AsyncNetworkStream, Error> {
// Unfortunately there doesn't currently seem to be a way to set the local address // Unfortunately, there doesn't currently seem to be a way to set the local address.
// Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have // Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have
// connected which is a blocking operation. // been connected, which is a blocking operation.
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>( async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
server: T, server: T,
timeout: Duration, timeout: Duration,
@@ -323,7 +323,7 @@ impl AsyncNetworkStream {
tcp_stream: Box<dyn AsyncTokioStream>, tcp_stream: Box<dyn AsyncTokioStream>,
tls_parameters: TlsParameters, tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> { ) -> Result<InnerAsyncNetworkStream, Error> {
let domain = tls_parameters.domain().to_string(); let domain = tls_parameters.domain().to_owned();
match tls_parameters.connector { match tls_parameters.connector {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]

View File

@@ -100,7 +100,7 @@ impl SmtpConnection {
mail_options.push(MailParameter::SmtpUtfEight); mail_options.push(MailParameter::SmtpUtfEight);
} }
// Check for non-ascii content in message // Check for non-ascii content in the message
if !email.is_ascii() { if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) { if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(error::client( return Err(error::client(
@@ -207,7 +207,7 @@ impl SmtpConnection {
self.command(Noop).is_ok() self.command(Noop).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 the challenge if needed
pub fn auth( pub fn auth(
&mut self, &mut self,
mechanisms: &[Mechanism], mechanisms: &[Mechanism],

View File

@@ -11,7 +11,7 @@
//! client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT, //! client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT,
//! }; //! };
//! //!
//! let hello = ClientId::Domain("my_hostname".to_string()); //! let hello = ClientId::Domain("my_hostname".to_owned());
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None, None)?; //! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None, None)?;
//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?; //! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
//! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?; //! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
@@ -38,7 +38,7 @@ pub(super) use self::tls::InnerTlsParameters;
pub use self::tls::TlsVersion; pub use self::tls::TlsVersion;
pub use self::{ pub use self::{
connection::SmtpConnection, connection::SmtpConnection,
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder}, tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
}; };
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]

View File

@@ -354,7 +354,7 @@ impl Write for NetworkStream {
} }
/// If the local address is set, binds the socket to this address. /// If the local address is set, binds the socket to this address.
/// If local address is not set, then destination address is required to determine to the default /// If local address is not set, then destination address is required to determine the default
/// local address on some platforms. /// local address on some platforms.
/// See: https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560 /// See: https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560
fn bind_local_address( fn bind_local_address(

View File

@@ -3,13 +3,16 @@ use std::fmt::{self, Debug};
use std::{sync::Arc, time::SystemTime}; use std::{sync::Arc, time::SystemTime};
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
use boring::ssl::{SslConnector, SslVersion}; use boring::{
ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder,
};
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ use rustls::{
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier}, client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName, ClientConfig, Error as TlsError, RootCertStore, ServerName,
}; };
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -44,9 +47,9 @@ pub enum TlsVersion {
Tlsv12, Tlsv12,
/// TLS 1.3 /// TLS 1.3
/// ///
/// The most secure option, altough not supported by all SMTP servers. /// The most secure option, although not supported by all SMTP servers.
/// ///
/// Altough it is technically supported by all TLS backends, /// Although it is technically supported by all TLS backends,
/// trying to set it for `native-tls` will give a runtime error. /// trying to set it for `native-tls` will give a runtime error.
Tlsv13, Tlsv13,
} }
@@ -94,6 +97,30 @@ impl Debug for Tls {
} }
} }
/// Source for the base set of root certificates to trust.
#[allow(missing_copy_implementations)]
#[derive(Clone, Debug, Default)]
pub enum CertificateStore {
/// Use the default for the TLS backend.
///
/// For native-tls, this will use the system certificate store on Windows, the keychain on
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`).
///
/// For rustls, this will also use the the system store if the `rustls-native-certs` feature is
/// enabled, or will fall back to `webpki-roots`.
///
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
#[default]
Default,
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
///
/// This option is only available in the rustls backend.
#[cfg(feature = "rustls-tls")]
WebpkiRoots,
/// Don't use any system certificates.
None,
}
/// Parameters to use for secure clients /// Parameters to use for secure clients
#[derive(Clone)] #[derive(Clone)]
pub struct TlsParameters { pub struct TlsParameters {
@@ -108,6 +135,7 @@ pub struct TlsParameters {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TlsParametersBuilder { pub struct TlsParametersBuilder {
domain: String, domain: String,
cert_store: CertificateStore,
root_certs: Vec<Certificate>, root_certs: Vec<Certificate>,
accept_invalid_hostnames: bool, accept_invalid_hostnames: bool,
accept_invalid_certs: bool, accept_invalid_certs: bool,
@@ -120,6 +148,7 @@ impl TlsParametersBuilder {
pub fn new(domain: String) -> Self { pub fn new(domain: String) -> Self {
Self { Self {
domain, domain,
cert_store: CertificateStore::Default,
root_certs: Vec::new(), root_certs: Vec::new(),
accept_invalid_hostnames: false, accept_invalid_hostnames: false,
accept_invalid_certs: false, accept_invalid_certs: false,
@@ -128,6 +157,12 @@ impl TlsParametersBuilder {
} }
} }
/// Set the source for the base set of root certificates to trust.
pub fn certificate_store(mut self, cert_store: CertificateStore) -> Self {
self.cert_store = cert_store;
self
}
/// Add a custom root certificate /// Add a custom root certificate
/// ///
/// Can be used to safely connect to a server using a self signed certificate, for example. /// Can be used to safely connect to a server using a self signed certificate, for example.
@@ -208,6 +243,18 @@ impl TlsParametersBuilder {
pub fn build_native(self) -> Result<TlsParameters, Error> { pub fn build_native(self) -> Result<TlsParameters, Error> {
let mut tls_builder = TlsConnector::builder(); let mut tls_builder = TlsConnector::builder();
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
tls_builder.disable_built_in_roots(true);
}
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in native tls"
)))
}
}
for cert in self.root_certs { for cert in self.root_certs {
tls_builder.add_root_certificate(cert.native_tls); tls_builder.add_root_certificate(cert.native_tls);
} }
@@ -246,6 +293,21 @@ impl TlsParametersBuilder {
if self.accept_invalid_certs { if self.accept_invalid_certs {
tls_builder.set_verify(SslVerifyMode::NONE); tls_builder.set_verify(SslVerifyMode::NONE);
} else { } else {
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
// Replace the default store with an empty store.
tls_builder
.set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build());
}
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in boring tls"
)))
}
}
let cert_store = tls_builder.cert_store_mut(); let cert_store = tls_builder.cert_store_mut();
for cert in self.root_certs { for cert in self.root_certs {
@@ -299,20 +361,60 @@ impl TlsParametersBuilder {
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {})) tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
} else { } else {
let mut root_cert_store = RootCertStore::empty(); let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
let mut valid_count = 0;
let mut invalid_count = 0;
for cert in native_certs {
match store.add(&rustls::Certificate(cert.0)) {
Ok(_) => valid_count += 1,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::debug!("certificate parsing failed: {:?}", err);
invalid_count += 1;
}
}
}
#[cfg(feature = "tracing")]
tracing::debug!(
"loaded platform certs with {valid_count} valid and {invalid_count} invalid certs"
);
Ok(())
}
#[cfg(feature = "rustls-tls")]
fn load_webpki_roots(store: &mut RootCertStore) {
// TODO: handle this in the rustls 0.22 upgrade
#[allow(deprecated)]
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
}
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store)?;
#[cfg(not(feature = "rustls-native-certs"))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(feature = "rustls-tls")]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
for cert in self.root_certs { for cert in self.root_certs {
for rustls_cert in cert.rustls { for rustls_cert in cert.rustls {
root_cert_store.add(&rustls_cert).map_err(error::tls)?; root_cert_store.add(&rustls_cert).map_err(error::tls)?;
} }
} }
root_cert_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
|ta| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
},
));
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new( tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
root_cert_store, root_cert_store,

View File

@@ -296,15 +296,15 @@ mod test {
#[test] #[test]
fn test_display() { fn test_display() {
let id = ClientId::Domain("localhost".to_string()); let id = ClientId::Domain("localhost".to_owned());
let email = Address::from_str("test@example.com").unwrap(); let email = Address::from_str("test@example.com").unwrap();
let mail_parameter = MailParameter::Other { let mail_parameter = MailParameter::Other {
keyword: "TEST".to_string(), keyword: "TEST".to_owned(),
value: Some("value".to_string()), value: Some("value".to_owned()),
}; };
let rcpt_parameter = RcptParameter::Other { let rcpt_parameter = RcptParameter::Other {
keyword: "TEST".to_string(), keyword: "TEST".to_owned(),
value: Some("value".to_string()), value: Some("value".to_owned()),
}; };
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n"); assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
assert_eq!( assert_eq!(
@@ -346,19 +346,13 @@ mod test {
assert_eq!(format!("{Noop}"), "NOOP\r\n"); assert_eq!(format!("{Noop}"), "NOOP\r\n");
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n"); assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
assert_eq!( assert_eq!(
format!("{}", Help::new(Some("test".to_string()))), format!("{}", Help::new(Some("test".to_owned()))),
"HELP test\r\n" "HELP test\r\n"
); );
assert_eq!( assert_eq!(format!("{}", Vrfy::new("test".to_owned())), "VRFY test\r\n");
format!("{}", Vrfy::new("test".to_string())), assert_eq!(format!("{}", Expn::new("test".to_owned())), "EXPN test\r\n");
"VRFY test\r\n"
);
assert_eq!(
format!("{}", Expn::new("test".to_string())),
"EXPN test\r\n"
);
assert_eq!(format!("{Rset}"), "RSET\r\n"); assert_eq!(format!("{Rset}"), "RSET\r\n");
let credentials = Credentials::new("user".to_string(), "password".to_string()); let credentials = Credentials::new("user".to_owned(), "password".to_owned());
assert_eq!( assert_eq!(
format!( format!(
"{}", "{}",

View File

@@ -0,0 +1,120 @@
use url::Url;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::client::{Tls, TlsParameters};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use super::AsyncSmtpTransportBuilder;
use super::{
authentication::Credentials, error, extension::ClientId, Error, SmtpTransportBuilder,
SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT,
};
pub(crate) trait TransportBuilder {
fn new<T: Into<String>>(server: T) -> Self;
fn tls(self, tls: super::Tls) -> Self;
fn port(self, port: u16) -> Self;
fn credentials(self, credentials: Credentials) -> Self;
fn hello_name(self, name: ClientId) -> Self;
}
impl TransportBuilder for SmtpTransportBuilder {
fn new<T: Into<String>>(server: T) -> Self {
Self::new(server)
}
fn tls(self, tls: super::Tls) -> Self {
self.tls(tls)
}
fn port(self, port: u16) -> Self {
self.port(port)
}
fn credentials(self, credentials: Credentials) -> Self {
self.credentials(credentials)
}
fn hello_name(self, name: ClientId) -> Self {
self.hello_name(name)
}
}
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
impl TransportBuilder for AsyncSmtpTransportBuilder {
fn new<T: Into<String>>(server: T) -> Self {
Self::new(server)
}
fn tls(self, tls: super::Tls) -> Self {
self.tls(tls)
}
fn port(self, port: u16) -> Self {
self.port(port)
}
fn credentials(self, credentials: Credentials) -> Self {
self.credentials(credentials)
}
fn hello_name(self, name: ClientId) -> Self {
self.hello_name(name)
}
}
/// Create a new SmtpTransportBuilder or AsyncSmtpTransportBuilder from a connection URL
pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) -> Result<B, Error> {
let connection_url = Url::parse(connection_url).map_err(error::connection)?;
let tls: Option<String> = connection_url
.query_pairs()
.find(|(k, _)| k == "tls")
.map(|(_, v)| v.to_string());
let host = connection_url
.host_str()
.ok_or_else(|| error::connection("smtp host undefined"))?;
let mut builder = B::new(host);
match (connection_url.scheme(), tls.as_deref()) {
("smtp", None) => {
builder = builder.port(connection_url.port().unwrap_or(SMTP_PORT));
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtp", Some("required")) => {
builder = builder
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
.tls(Tls::Required(TlsParameters::new(host.into())?))
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtp", Some("opportunistic")) => {
builder = builder
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
.tls(Tls::Opportunistic(TlsParameters::new(host.into())?))
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtps", _) => {
builder = builder
.port(connection_url.port().unwrap_or(SUBMISSIONS_PORT))
.tls(Tls::Wrapper(TlsParameters::new(host.into())?))
}
(scheme, tls) => {
return Err(error::connection(format!(
"Unknown scheme '{scheme}' or tls parameter '{tls:?}', note that a transport with TLS requires one of the TLS features"
)))
}
};
// use the path segment of the URL as name in the name in the HELO / EHLO command
if connection_url.path().len() > 1 {
let name = connection_url.path().trim_matches('/').to_owned();
builder = builder.hello_name(ClientId::Domain(name));
}
if let Some(password) = connection_url.password() {
let credentials = Credentials::new(connection_url.username().into(), password.into());
builder = builder.credentials(credentials);
}
Ok(builder)
}

View File

@@ -119,7 +119,7 @@ pub struct ServerInfo {
impl Display for ServerInfo { impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let features = if self.features.is_empty() { let features = if self.features.is_empty() {
"no supported features".to_string() "no supported features".to_owned()
} else { } else {
format!("{:?}", self.features) format!("{:?}", self.features)
}; };
@@ -174,7 +174,7 @@ impl ServerInfo {
} }
Ok(ServerInfo { Ok(ServerInfo {
name: name.to_string(), name: name.to_owned(),
features, features,
}) })
} }
@@ -190,7 +190,7 @@ impl ServerInfo {
.contains(&Extension::Authentication(mechanism)) .contains(&Extension::Authentication(mechanism))
} }
/// Gets a compatible mechanism from list /// Gets a compatible mechanism from a list
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> { pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
for mechanism in mechanisms { for mechanism in mechanisms {
if self.supports_auth_mechanism(*mechanism) { if self.supports_auth_mechanism(*mechanism) {
@@ -281,7 +281,7 @@ impl Display for RcptParameter {
RcptParameter::Other { RcptParameter::Other {
ref keyword, ref keyword,
value: Some(ref value), value: Some(ref value),
} => write!(f, "{}={}", keyword, XText(value)), } => write!(f, "{keyword}={}", XText(value)),
RcptParameter::Other { RcptParameter::Other {
ref keyword, ref keyword,
value: None, value: None,
@@ -304,21 +304,21 @@ mod test {
#[test] #[test]
fn test_clientid_fmt() { fn test_clientid_fmt() {
assert_eq!( assert_eq!(
format!("{}", ClientId::Domain("test".to_string())), format!("{}", ClientId::Domain("test".to_owned())),
"test".to_string() "test".to_owned()
); );
assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_string()); assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_owned());
} }
#[test] #[test]
fn test_extension_fmt() { fn test_extension_fmt() {
assert_eq!( assert_eq!(
format!("{}", Extension::EightBitMime), format!("{}", Extension::EightBitMime),
"8BITMIME".to_string() "8BITMIME".to_owned()
); );
assert_eq!( assert_eq!(
format!("{}", Extension::Authentication(Mechanism::Plain)), format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string() "AUTH PLAIN".to_owned()
); );
} }
@@ -331,11 +331,11 @@ mod test {
format!( format!(
"{}", "{}",
ServerInfo { ServerInfo {
name: "name".to_string(), name: "name".to_owned(),
features: eightbitmime, features: eightbitmime,
} }
), ),
"name with {EightBitMime}".to_string() "name with {EightBitMime}".to_owned()
); );
let empty = HashSet::new(); let empty = HashSet::new();
@@ -344,11 +344,11 @@ mod test {
format!( format!(
"{}", "{}",
ServerInfo { ServerInfo {
name: "name".to_string(), name: "name".to_owned(),
features: empty, features: empty,
} }
), ),
"name with no supported features".to_string() "name with no supported features".to_owned()
); );
let mut plain = HashSet::new(); let mut plain = HashSet::new();
@@ -358,11 +358,11 @@ mod test {
format!( format!(
"{}", "{}",
ServerInfo { ServerInfo {
name: "name".to_string(), name: "name".to_owned(),
features: plain, features: plain,
} }
), ),
"name with {Authentication(Plain)}".to_string() "name with {Authentication(Plain)}".to_owned()
); );
} }
@@ -374,18 +374,14 @@ mod test {
Category::Unspecified4, Category::Unspecified4,
Detail::One, Detail::One,
), ),
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
); );
let mut features = HashSet::new(); let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime)); assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo { let server_info = ServerInfo {
name: "me".to_string(), name: "me".to_owned(),
features, features,
}; };
@@ -401,10 +397,10 @@ mod test {
Detail::One, Detail::One,
), ),
vec![ vec![
"me".to_string(), "me".to_owned(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(), "AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
], ],
); );
@@ -414,7 +410,7 @@ mod test {
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),)); assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
let server_info2 = ServerInfo { let server_info2 = ServerInfo {
name: "me".to_string(), name: "me".to_owned(),
features: features2, features: features2,
}; };

View File

@@ -77,8 +77,8 @@
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")? //! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Add credentials for authentication //! // Add credentials for authentication
//! .credentials(Credentials::new( //! .credentials(Credentials::new(
//! "username".to_string(), //! "username".to_owned(),
//! "password".to_string(), //! "password".to_owned(),
//! )) //! ))
//! // Configure expected authentication mechanism //! // Configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain]) //! .authentication(vec![Mechanism::Plain])
@@ -111,7 +111,7 @@
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! // Custom TLS configuration //! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_string()) //! let tls = TlsParameters::builder("smtp.example.com".to_owned())
//! .dangerous_accept_invalid_certs(true) //! .dangerous_accept_invalid_certs(true)
//! .build()?; //! .build()?;
//! //!
@@ -154,6 +154,7 @@ mod async_transport;
pub mod authentication; pub mod authentication;
pub mod client; pub mod client;
pub mod commands; pub mod commands;
mod connection_url;
mod error; mod error;
pub mod extension; pub mod extension;
#[cfg(feature = "pool")] #[cfg(feature = "pool")]
@@ -200,7 +201,7 @@ struct SmtpInfo {
impl Default for SmtpInfo { impl Default for SmtpInfo {
fn default() -> Self { fn default() -> Self {
Self { Self {
server: "localhost".to_string(), server: "localhost".to_owned(),
port: SMTP_PORT, port: SMTP_PORT,
hello_name: ClientId::default(), hello_name: ClientId::default(),
credentials: None, credentials: None,

View File

@@ -203,7 +203,7 @@ impl<E: Executor> Debug for Pool<E> {
&match self.connections.try_lock() { &match self.connections.try_lock() {
Some(connections) => format!("{} connections", connections.len()), Some(connections) => format!("{} connections", connections.len()),
None => "LOCKED".to_string(), None => "LOCKED".to_owned(),
}, },
) )
.field("client", &self.client) .field("client", &self.client)

View File

@@ -186,8 +186,8 @@ impl Debug for Pool {
&match self.connections.try_lock() { &match self.connections.try_lock() {
Ok(connections) => format!("{} connections", connections.len()), Ok(connections) => format!("{} connections", connections.len()),
Err(TryLockError::WouldBlock) => "LOCKED".to_string(), Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
Err(TryLockError::Poisoned(_)) => "POISONED".to_string(), Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
}, },
) )
.field("client", &self.client) .field("client", &self.client)

View File

@@ -19,7 +19,7 @@ use nom::{
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// First digit indicates severity /// The first digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)] #[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity { pub enum Severity {
@@ -151,7 +151,7 @@ impl FromStr for Response {
fn from_str(s: &str) -> result::Result<Response, Error> { fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s) parse_response(s)
.map(|(_, r)| r) .map(|(_, r)| r)
.map_err(|e| error::response(e.to_string())) .map_err(|e| error::response(e.to_owned()))
} }
} }
@@ -329,10 +329,10 @@ mod test {
detail: Detail::Zero, detail: Detail::Zero,
}, },
message: vec![ message: vec![
"me".to_string(), "me".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
"AUTH PLAIN CRAM-MD5".to_string(), "AUTH PLAIN CRAM-MD5".to_owned(),
], ],
} }
); );
@@ -352,11 +352,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::Zero, detail: Detail::Zero,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.is_positive()); .is_positive());
assert!(!Response::new( assert!(!Response::new(
@@ -365,11 +361,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::Zero, detail: Detail::Zero,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.is_positive()); .is_positive());
} }
@@ -382,11 +374,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.has_code(451)); .has_code(451));
assert!(!Response::new( assert!(!Response::new(
@@ -395,11 +383,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.has_code(251)); .has_code(251));
} }
@@ -413,11 +397,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.first_word(), .first_word(),
Some("me") Some("me")
@@ -430,9 +410,9 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec![
"me mo".to_string(), "me mo".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
], ],
) )
.first_word(), .first_word(),
@@ -457,7 +437,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_word(), .first_word(),
None None
@@ -469,7 +449,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_word(), .first_word(),
None None
@@ -481,7 +461,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec!["".to_string()], vec!["".to_owned()],
) )
.first_word(), .first_word(),
None None
@@ -507,11 +487,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.first_line(), .first_line(),
Some("me") Some("me")
@@ -524,9 +500,9 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec![
"me mo".to_string(), "me mo".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
], ],
) )
.first_line(), .first_line(),
@@ -551,7 +527,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_line(), .first_line(),
Some(" ") Some(" ")
@@ -563,7 +539,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_line(), .first_line(),
Some(" ") Some(" ")
@@ -575,7 +551,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec!["".to_string()], vec!["".to_owned()],
) )
.first_line(), .first_line(),
Some("") Some("")

View File

@@ -58,7 +58,7 @@ impl SmtpTransport {
.tls(Tls::Wrapper(tls_parameters))) .tls(Tls::Wrapper(tls_parameters)))
} }
/// Simple an secure transport, using STARTTLS to obtain encrypted connections /// Simple and secure transport, using STARTTLS to obtain encrypted connections
/// ///
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers /// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections. /// that don't take SMTPS connections.
@@ -95,29 +95,106 @@ impl SmtpTransport {
/// ///
/// * No authentication /// * No authentication
/// * No TLS /// * No TLS
/// * A 60 seconds timeout for smtp commands /// * A 60-seconds timeout for smtp commands
/// * Port 25 /// * Port 25
/// ///
/// Consider using [`SmtpTransport::relay`](#method.relay) or /// Consider using [`SmtpTransport::relay`](#method.relay) or
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead, /// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible. /// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder { pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let new = SmtpInfo { SmtpTransportBuilder::new(server)
server: server.into(), }
..Default::default()
};
SmtpTransportBuilder { /// Creates a `SmtpTransportBuilder` from a connection URL
info: new, ///
#[cfg(feature = "pool")] /// The protocol, credentials, host and port can be provided in a single URL.
pool_config: PoolConfig::default(), /// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
} /// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
/// The path section of the url can be used to set an alternative name for
/// the HELO / EHLO command.
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
/// will set the HELO / EHLO name `client.example.com`.
///
/// <table>
/// <thead>
/// <tr>
/// <th>scheme</th>
/// <th>tls parameter</th>
/// <th>example</th>
/// <th>remarks</th>
/// </tr>
/// </thead>
/// <tbody>
/// <tr>
/// <td>smtps</td>
/// <td>-</td>
/// <td>smtps://smtp.example.com</td>
/// <td>SMTP over TLS, recommended method</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>required</td>
/// <td>smtp://smtp.example.com?tls=required</td>
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>opportunistic</td>
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
/// <td>
/// SMTP with optionally STARTTLS when supported by the server.
/// Caution: this method is vulnerable to a man-in-the-middle attack.
/// Not recommended for production use.
/// </td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>-</td>
/// <td>smtp://smtp.example.com</td>
/// <td>Unencrypted SMTP, not recommended for production use.</td>
/// </tr>
/// </tbody>
/// </table>
///
/// ```rust,no_run
/// use lettre::{
/// message::header::ContentType, transport::smtp::authentication::Credentials, Message,
/// SmtpTransport, Transport,
/// };
///
/// let email = Message::builder()
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
/// .to("Hei <hei@domain.tld>".parse().unwrap())
/// .subject("Happy new year")
/// .header(ContentType::TEXT_PLAIN)
/// .body(String::from("Be happy!"))
/// .unwrap();
///
/// // Open a remote connection to example
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com:465")
/// .unwrap()
/// .build();
///
/// // Send the email
/// match mailer.send(&email) {
/// Ok(_) => println!("Email sent successfully!"),
/// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// ```
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> {
super::connection_url::from_connection_url(connection_url)
} }
/// Tests the SMTP connection /// Tests the SMTP connection
/// ///
/// `test_connection()` tests the connection by using the SMTP NOOP command. /// `test_connection()` tests the connection by using the SMTP NOOP command.
/// The connection is closed afterwards if a connection pool is not used. /// The connection is closed afterward if a connection pool is not used.
pub fn test_connection(&self) -> Result<bool, Error> { pub fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection()?; let mut conn = self.inner.connection()?;
@@ -141,6 +218,20 @@ pub struct SmtpTransportBuilder {
/// Builder for the SMTP `SmtpTransport` /// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder { impl SmtpTransportBuilder {
// Create new builder with default parameters
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
let new = SmtpInfo {
server: server.into(),
..Default::default()
};
Self {
info: new,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Set the name used during EHLO /// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self { pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name; self.info.hello_name = name;
@@ -194,7 +285,7 @@ impl SmtpTransportBuilder {
/// Build the transport /// Build the transport
/// ///
/// If the `pool` feature is enabled an `Arc` wrapped pool is be created. /// If the `pool` feature is enabled, an `Arc` wrapped pool is created.
/// Defaults can be found at [`PoolConfig`] /// Defaults can be found at [`PoolConfig`]
pub fn build(self) -> SmtpTransport { pub fn build(self) -> SmtpTransport {
let client = SmtpClient { info: self.info }; let client = SmtpClient { info: self.info };
@@ -252,3 +343,62 @@ impl SmtpClient {
Ok(conn) Ok(conn)
} }
} }
#[cfg(test)]
mod tests {
use crate::{
transport::smtp::{authentication::Credentials, client::Tls},
SmtpTransport,
};
#[test]
fn transport_from_url() {
let builder = SmtpTransport::from_url("smtp://127.0.0.1:2525").unwrap();
assert_eq!(builder.info.port, 2525);
assert!(matches!(builder.info.tls, Tls::None));
assert_eq!(builder.info.server, "127.0.0.1");
let builder =
SmtpTransport::from_url("smtps://username:password@smtp.example.com:465").unwrap();
assert_eq!(builder.info.port, 465);
assert_eq!(
builder.info.credentials,
Some(Credentials::new(
"username".to_owned(),
"password".to_owned()
))
);
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
assert_eq!(builder.info.server, "smtp.example.com");
let builder =
SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
.unwrap();
assert_eq!(builder.info.port, 587);
assert_eq!(
builder.info.credentials,
Some(Credentials::new(
"username".to_owned(),
"password".to_owned()
))
);
assert!(matches!(builder.info.tls, Tls::Required(_)));
let builder = SmtpTransport::from_url(
"smtp://username:password@smtp.example.com:587?tls=opportunistic",
)
.unwrap();
assert_eq!(builder.info.port, 587);
assert!(matches!(builder.info.tls, Tls::Opportunistic(_)));
let builder = SmtpTransport::from_url("smtps://smtp.example.com").unwrap();
assert_eq!(builder.info.port, 465);
assert_eq!(builder.info.credentials, None);
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
}
}

View File

@@ -41,7 +41,7 @@ mod tests {
] ]
.iter() .iter()
{ {
assert_eq!(format!("{}", XText(input)), (*expect).to_string()); assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
} }
} }
} }