Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efee4b5d72 | ||
|
|
57d7bf25cc | ||
|
|
c52c596458 | ||
|
|
53dee3e31f | ||
|
|
c64cb0ff2e | ||
|
|
10d7b197ed | ||
|
|
fb54855d5f | ||
|
|
157c4fb5ae | ||
|
|
1196e332ee | ||
|
|
75770f7bc6 | ||
|
|
76d0929c94 | ||
|
|
c3d00051b2 | ||
|
|
12580d82f4 | ||
|
|
f7849078b8 | ||
|
|
f2c94cdf4d | ||
|
|
74f64b81ab | ||
|
|
39c71dbfd2 | ||
|
|
c1bf5dfda1 | ||
|
|
1c1fef8055 | ||
|
|
1540f16015 | ||
|
|
330daa1173 | ||
|
|
47f2fe0750 | ||
|
|
8b6cee30ee | ||
|
|
62c16e90ef | ||
|
|
e0494a5f9d | ||
|
|
8c3bffa728 | ||
|
|
47eda90433 | ||
|
|
46ea8c48ac | ||
|
|
5f7063fdc3 | ||
|
|
61c1f6bc6f | ||
|
|
283e21f8d6 | ||
|
|
20c3701eb0 | ||
|
|
74117d5cc6 | ||
|
|
bb49e0a46b | ||
|
|
42365478c2 | ||
|
|
94769242d1 | ||
|
|
7e6ffe8aea | ||
|
|
16c35ef583 | ||
|
|
bbab86b484 | ||
|
|
b5652f18b7 | ||
|
|
c2f2b907a9 | ||
|
|
a1cc770613 | ||
|
|
57886c367d | ||
|
|
f3a469431e | ||
|
|
9b48ef355b | ||
|
|
7fee8dc5a8 | ||
|
|
7e9fff9bd0 | ||
|
|
92f5460132 | ||
|
|
cd0c032f71 | ||
|
|
f41c9c19ab | ||
|
|
cb6a7178d9 | ||
|
|
2bfc759aa3 | ||
|
|
89673d0eb2 | ||
|
|
8b588cf275 | ||
|
|
5f37b66352 | ||
|
|
69e5974024 | ||
|
|
4fb67a7da1 | ||
|
|
9041f210f4 | ||
|
|
77b7d40fb8 | ||
|
|
2b6d457f85 | ||
|
|
952c1b39df |
17
.github/workflows/test.yml
vendored
17
.github/workflows/test.yml
vendored
@@ -13,7 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: rustfmt / nightly-2022-11-12
|
||||
name: rustfmt / nightly-2023-06-22
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup default nightly-2022-11-12
|
||||
rustup default nightly-2023-06-22
|
||||
rustup component add rustfmt
|
||||
|
||||
- name: cargo fmt
|
||||
@@ -75,8 +75,8 @@ jobs:
|
||||
rust: stable
|
||||
- name: beta
|
||||
rust: beta
|
||||
- name: 1.60.0
|
||||
rust: 1.60.0
|
||||
- name: '1.70'
|
||||
rust: '1.70'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -113,7 +113,10 @@ jobs:
|
||||
run: sudo apt -y install python3-dkim
|
||||
|
||||
- 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
|
||||
run: cargo test --no-default-features
|
||||
@@ -122,10 +125,10 @@ jobs:
|
||||
run: cargo test
|
||||
|
||||
- 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)
|
||||
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:
|
||||
# name: Coverage
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,4 +4,3 @@
|
||||
lettre.sublime-*
|
||||
lettre.iml
|
||||
target/
|
||||
/Cargo.lock
|
||||
|
||||
136
CHANGELOG.md
136
CHANGELOG.md
@@ -1,3 +1,137 @@
|
||||
<a name="v0.11.4"></a>
|
||||
### v0.11.4 (2024-01-28)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Percent decode credentials in SMTP connect URL ([#932], [#934])
|
||||
* Fix mimebody DKIM body-hash computation ([#923])
|
||||
|
||||
[#923]: https://github.com/lettre/lettre/pull/923
|
||||
[#932]: https://github.com/lettre/lettre/pull/932
|
||||
[#934]: https://github.com/lettre/lettre/pull/934
|
||||
|
||||
<a name="v0.11.3"></a>
|
||||
### v0.11.3 (2024-01-02)
|
||||
|
||||
#### Features
|
||||
|
||||
* Derive `Clone` for `FileTransport` and `AsyncFileTransport` ([#924])
|
||||
* Derive `Debug` for `SmtpTransport` ([#925])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Upgrade `rustls` to v0.22 ([#921])
|
||||
* Drop once_cell dependency in favor of OnceLock from std ([#928])
|
||||
|
||||
[#921]: https://github.com/lettre/lettre/pull/921
|
||||
[#924]: https://github.com/lettre/lettre/pull/924
|
||||
[#925]: https://github.com/lettre/lettre/pull/925
|
||||
[#928]: https://github.com/lettre/lettre/pull/928
|
||||
|
||||
<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>
|
||||
### v0.10.2 (2023-01-29)
|
||||
|
||||
@@ -110,7 +244,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
|
||||
* Update `hostname` to 0.3
|
||||
* Update to `nom` 6
|
||||
* Replace `log` with `tracing`
|
||||
* Move CI to Github Actions
|
||||
* Move CI to GitHub Actions
|
||||
* Use criterion for benchmarks
|
||||
|
||||
<a name="v0.9.2"></a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 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
|
||||
|
||||
|
||||
2660
Cargo.lock
generated
Normal file
2660
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
79
Cargo.toml
79
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lettre"
|
||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||
version = "0.10.2"
|
||||
version = "0.11.4"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "https://lettre.rs"
|
||||
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
|
||||
categories = ["email", "network-programming"]
|
||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||
edition = "2021"
|
||||
rust-version = "1.60"
|
||||
rust-version = "1.70"
|
||||
|
||||
[badges]
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||
@@ -19,16 +19,16 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
idna = "0.3"
|
||||
once_cell = { version = "1", optional = true }
|
||||
chumsky = "0.9"
|
||||
idna = "0.5"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
||||
|
||||
# builder
|
||||
httpdate = { version = "1", optional = true }
|
||||
mime = { version = "0.3.4", optional = true }
|
||||
fastrand = { version = "1.4", optional = true }
|
||||
quoted_printable = { version = "0.4.6", optional = true }
|
||||
base64 = { version = "0.21", optional = true }
|
||||
fastrand = { version = "2.0", optional = true }
|
||||
quoted_printable = { version = "0.5", optional = true }
|
||||
base64 = { version = "0.22", optional = true }
|
||||
email-encoding = { version = "0.2", optional = true }
|
||||
|
||||
# file transport
|
||||
@@ -39,14 +39,17 @@ serde_json = { version = "1", optional = true }
|
||||
# smtp-transport
|
||||
nom = { version = "7", optional = true }
|
||||
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 }
|
||||
percent-encoding = { version = "2.3", optional = true }
|
||||
|
||||
## tls
|
||||
native-tls = { version = "0.2", optional = true } # feature
|
||||
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true }
|
||||
rustls-pemfile = { version = "1", optional = true }
|
||||
webpki-roots = { version = "0.22", optional = true }
|
||||
boring = { version = "2.0.0", optional = true }
|
||||
native-tls = { version = "0.2.5", optional = true } # feature
|
||||
rustls = { version = "0.22.1", optional = true }
|
||||
rustls-pemfile = { version = "2", optional = true }
|
||||
rustls-native-certs = { version = "0.7", optional = true }
|
||||
webpki-roots = { version = "0.26", optional = true }
|
||||
boring = { version = "4", optional = true }
|
||||
|
||||
# async
|
||||
futures-io = { version = "0.3.7", optional = true }
|
||||
@@ -56,25 +59,25 @@ async-trait = { version = "0.1", optional = true }
|
||||
## async-std
|
||||
async-std = { version = "1.8", optional = true }
|
||||
#async-native-tls = { version = "0.3.3", optional = true }
|
||||
futures-rustls = { version = "0.22", optional = true }
|
||||
futures-rustls = { version = "0.25", optional = true }
|
||||
|
||||
## tokio
|
||||
tokio1_crate = { package = "tokio", version = "1", 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_boring = { package = "tokio-boring", version = "2.1.4", optional = true }
|
||||
tokio1_rustls = { package = "tokio-rustls", version = "0.25", optional = true }
|
||||
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
|
||||
|
||||
## dkim
|
||||
sha2 = { version = "0.10", optional = true, features = ["oid"] }
|
||||
rsa = { version = "0.8", optional = true }
|
||||
ed25519-dalek = { version = "1.0.1", optional = true }
|
||||
rsa = { version = "0.9", optional = true }
|
||||
ed25519-dalek = { version = "2", optional = true }
|
||||
|
||||
# email formats
|
||||
email_address = { version = "0.2.1", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
criterion = "0.4"
|
||||
criterion = "0.5"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
|
||||
tracing-subscriber = "0.3"
|
||||
glob = "0.3"
|
||||
@@ -82,39 +85,43 @@ walkdir = "2"
|
||||
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
async-std = { version = "1.8", features = ["attributes"] }
|
||||
serde_json = "1"
|
||||
maud = "0.24"
|
||||
maud = "0.26"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "transport_smtp"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "mailbox_parsing"
|
||||
|
||||
[features]
|
||||
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
|
||||
builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"]
|
||||
mime03 = ["mime"]
|
||||
builder = ["dep:httpdate", "dep:mime", "dep:fastrand", "dep:quoted_printable", "dep:email-encoding"]
|
||||
mime03 = ["dep:mime"]
|
||||
|
||||
# transports
|
||||
file-transport = ["uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||
file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
||||
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
|
||||
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:url", "dep:percent-encoding", "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-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
|
||||
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
|
||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "futures-rustls"]
|
||||
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
|
||||
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
||||
tokio1-boring-tls = ["tokio1", "boring-tls", "tokio1_boring"]
|
||||
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
|
||||
#async-std1-native-tls = ["async-std1", "native-tls", "dep:async-native-tls"]
|
||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
|
||||
tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
|
||||
tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
|
||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "dep:tokio1_rustls"]
|
||||
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]
|
||||
all-features = true
|
||||
|
||||
16
README.md
16
README.md
@@ -28,8 +28,8 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.10.2">
|
||||
<img src="https://deps.rs/crate/lettre/0.10.2/status.svg"
|
||||
<a href="https://deps.rs/crate/lettre/0.11.4">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.4/status.svg"
|
||||
alt="dependency status" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -53,20 +53,21 @@ Lettre does not provide (for now):
|
||||
## Supported Rust Versions
|
||||
|
||||
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.
|
||||
|
||||
## 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`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.10"
|
||||
lettre = "0.11"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
@@ -75,10 +76,11 @@ let email = Message::builder()
|
||||
.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();
|
||||
|
||||
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
|
||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
@@ -89,7 +91,7 @@ let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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
|
||||
should not be reported via the public Github Issue tracker.
|
||||
should not be reported via the public GitHub Issue tracker.
|
||||
|
||||
## Security advisories
|
||||
|
||||
|
||||
27
benches/mailbox_parsing.rs
Normal file
27
benches/mailbox_parsing.rs
Normal 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);
|
||||
@@ -1,6 +1,6 @@
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
|
||||
AsyncTransport, Message,
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncStd1Executor, AsyncTransport, Message,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
@@ -12,10 +12,11 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.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
|
||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
|
||||
AsyncTransport, Message,
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncStd1Executor, AsyncTransport, Message,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
@@ -12,10 +12,11 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.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
|
||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
@@ -8,6 +8,7 @@ fn main() {
|
||||
.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();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fs;
|
||||
|
||||
use lettre::{
|
||||
message::header::ContentType,
|
||||
transport::smtp::{
|
||||
authentication::Credentials,
|
||||
client::{Certificate, Tls, TlsParameters},
|
||||
@@ -16,18 +17,19 @@ fn main() {
|
||||
.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();
|
||||
|
||||
// Use a custom certificate stored on disk to securely verify the server's certificate
|
||||
let pem_cert = fs::read("certificate.pem").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)
|
||||
.build()
|
||||
.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
|
||||
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
|
||||
|
||||
@@ -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() {
|
||||
tracing_subscriber::fmt::init();
|
||||
@@ -8,10 +11,11 @@ fn main() {
|
||||
.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();
|
||||
|
||||
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
|
||||
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
|
||||
|
||||
@@ -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() {
|
||||
tracing_subscriber::fmt::init();
|
||||
@@ -8,10 +11,11 @@ fn main() {
|
||||
.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();
|
||||
|
||||
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
|
||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
|
||||
Tokio1Executor,
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
@@ -16,10 +16,11 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.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
|
||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
|
||||
Tokio1Executor,
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
@@ -16,10 +16,11 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.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
|
||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
|
||||
@@ -15,7 +15,7 @@ use idna::domain_to_ascii;
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
@@ -227,6 +227,7 @@ fn check_address(val: &str) -> Result<usize, AddressError> {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
/// Errors in email addresses parsing
|
||||
pub enum AddressError {
|
||||
/// Missing domain or user
|
||||
@@ -237,6 +238,8 @@ pub enum AddressError {
|
||||
InvalidUser,
|
||||
/// Invalid email domain
|
||||
InvalidDomain,
|
||||
/// Invalid input found
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
impl Error for AddressError {}
|
||||
@@ -248,6 +251,7 @@ impl Display for AddressError {
|
||||
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
|
||||
AddressError::InvalidUser => f.write_str("Invalid email user"),
|
||||
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
|
||||
AddressError::InvalidInput => f.write_str("Invalid input"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ impl Executor for Tokio1Executor {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
@@ -149,13 +149,13 @@ impl Executor for Tokio1Executor {
|
||||
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
Tls::Required(tls_parameters) => {
|
||||
conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
@@ -216,7 +216,7 @@ impl Executor for AsyncStd1Executor {
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ impl Executor for AsyncStd1Executor {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
@@ -245,13 +245,13 @@ impl Executor for AsyncStd1Executor {
|
||||
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
Tls::Required(tls_parameters) => {
|
||||
conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@@ -6,7 +6,7 @@
|
||||
//! * Secure defaults
|
||||
//! * Async support
|
||||
//!
|
||||
//! Lettre requires Rust 1.60 or newer.
|
||||
//! Lettre requires Rust 1.70 or newer.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
@@ -109,7 +109,7 @@
|
||||
//! [mime 0.3]: https://docs.rs/mime/0.3
|
||||
//! [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.4")]
|
||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -135,6 +135,8 @@
|
||||
clippy::manual_assert,
|
||||
clippy::unnecessary_join,
|
||||
clippy::wildcard_imports,
|
||||
clippy::str_to_string,
|
||||
clippy::empty_structs_with_brackets,
|
||||
clippy::zero_sized_map_values
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
@@ -217,11 +219,11 @@ use std::error::Error as StdError;
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
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;
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::executor::Tokio1Executor;
|
||||
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[doc(inline)]
|
||||
pub use self::transport::AsyncTransport;
|
||||
pub use crate::address::Address;
|
||||
|
||||
@@ -13,9 +13,9 @@ pub struct Attachment {
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Disposition {
|
||||
/// file name
|
||||
/// File name
|
||||
Attached(String),
|
||||
/// content id
|
||||
/// Content id
|
||||
Inline(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -496,7 +496,7 @@ mod test {
|
||||
#[test]
|
||||
fn base64_encode_bytes_wrapping() {
|
||||
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,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::{
|
||||
borrow::Cow,
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display},
|
||||
iter::IntoIterator,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
@@ -108,7 +107,7 @@ pub struct DkimSigningKey(InnerDkimSigningKey);
|
||||
#[derive(Debug)]
|
||||
enum InnerDkimSigningKey {
|
||||
Rsa(RsaPrivateKey),
|
||||
Ed25519(ed25519_dalek::Keypair),
|
||||
Ed25519(ed25519_dalek::SigningKey),
|
||||
}
|
||||
|
||||
impl DkimSigningKey {
|
||||
@@ -121,14 +120,18 @@ impl DkimSigningKey {
|
||||
RsaPrivateKey::from_pkcs1_pem(private_key)
|
||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
|
||||
),
|
||||
DkimSigningAlgorithm::Ed25519 => InnerDkimSigningKey::Ed25519(
|
||||
ed25519_dalek::Keypair::from_bytes(
|
||||
&crate::base64::decode(private_key).map_err(|err| {
|
||||
DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err))
|
||||
})?,
|
||||
)
|
||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?,
|
||||
),
|
||||
DkimSigningAlgorithm::Ed25519 => {
|
||||
InnerDkimSigningKey::Ed25519(ed25519_dalek::SigningKey::from_bytes(
|
||||
&crate::base64::decode(private_key)
|
||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)))?
|
||||
.try_into()
|
||||
.map_err(|_| {
|
||||
DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(
|
||||
ed25519_dalek::ed25519::Error::new(),
|
||||
))
|
||||
})?,
|
||||
))
|
||||
}
|
||||
}))
|
||||
}
|
||||
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
|
||||
@@ -140,19 +143,18 @@ impl DkimSigningKey {
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub struct DkimConfig {
|
||||
/// The name of the key published in DNS
|
||||
selector: String,
|
||||
/// The domain for which we sign the message
|
||||
domain: String,
|
||||
/// The private key in PKCS1 string format
|
||||
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>,
|
||||
/// The signing algorithm to be used when signing
|
||||
canonicalization: DkimCanonicalization,
|
||||
}
|
||||
|
||||
@@ -341,7 +343,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
|
||||
|
||||
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
|
||||
@@ -477,9 +479,9 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
||||
.from("Test O'Leary <test+ezrz@example.net>".parse().unwrap())
|
||||
.to("Test2 <test2@example.org>".parse().unwrap())
|
||||
.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 ë")
|
||||
.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]
|
||||
@@ -521,8 +523,8 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
||||
dkim_sign_fixed_time(
|
||||
&mut message,
|
||||
&DkimConfig::new(
|
||||
"dkimtest".to_string(),
|
||||
"example.org".to_string(),
|
||||
"dkimtest".to_owned(),
|
||||
"example.org".to_owned(),
|
||||
signing_key,
|
||||
vec![
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
@@ -570,8 +572,8 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
||||
dkim_sign_fixed_time(
|
||||
&mut message,
|
||||
&DkimConfig::new(
|
||||
"dkimtest".to_string(),
|
||||
"example.org".to_string(),
|
||||
"dkimtest".to_owned(),
|
||||
"example.org".to_owned(),
|
||||
signing_key,
|
||||
vec![
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
|
||||
@@ -13,12 +13,14 @@ use crate::BoxError;
|
||||
/// use-caches this header shouldn't be set manually.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Default)]
|
||||
pub enum ContentTransferEncoding {
|
||||
/// ASCII
|
||||
SevenBit,
|
||||
/// Quoted-Printable encoding
|
||||
QuotedPrintable,
|
||||
/// base64 encoding
|
||||
#[default]
|
||||
Base64,
|
||||
/// Requires `8BITMIME`
|
||||
EightBit,
|
||||
@@ -67,12 +69,6 @@ impl FromStr for ContentTransferEncoding {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContentTransferEncoding {
|
||||
fn default() -> Self {
|
||||
ContentTransferEncoding::Base64
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -99,7 +95,7 @@ mod test {
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"7bit".to_string(),
|
||||
"7bit".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -109,7 +105,7 @@ mod test {
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"base64".to_string(),
|
||||
"base64".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -16,13 +16,13 @@ impl ContentDisposition {
|
||||
pub fn inline() -> Self {
|
||||
Self(HeaderValue::dangerous_new_pre_encoded(
|
||||
Self::name(),
|
||||
"inline".to_string(),
|
||||
"inline".to_string(),
|
||||
"inline".to_owned(),
|
||||
"inline".to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
Self::with_name("inline", file_name)
|
||||
}
|
||||
@@ -106,7 +106,7 @@ mod test {
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||
"inline".to_string(),
|
||||
"inline".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -116,7 +116,7 @@ mod test {
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||
"attachment; filename=\"something.txt\"".to_string(),
|
||||
"attachment; filename=\"something.txt\"".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -11,12 +11,12 @@ use crate::BoxError;
|
||||
|
||||
/// `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.
|
||||
///
|
||||
/// 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)]
|
||||
pub struct ContentType(Mime);
|
||||
|
||||
@@ -178,14 +178,14 @@ mod test {
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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));
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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));
|
||||
|
||||
@@ -90,7 +90,7 @@ mod test {
|
||||
|
||||
assert_eq!(
|
||||
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
|
||||
@@ -110,7 +110,7 @@ mod test {
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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!(
|
||||
@@ -122,7 +122,7 @@ mod test {
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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!(
|
||||
|
||||
@@ -252,7 +252,7 @@ mod test {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"kayo@example.com".to_string(),
|
||||
"kayo@example.com".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(From(from)));
|
||||
@@ -265,7 +265,7 @@ mod test {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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)));
|
||||
@@ -281,7 +281,7 @@ mod test {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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())));
|
||||
@@ -297,7 +297,7 @@ mod test {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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())));
|
||||
@@ -306,14 +306,30 @@ mod test {
|
||||
#[test]
|
||||
fn parse_multi_with_name_containing_comma() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"Test, test <1@example.com>".parse().unwrap(),
|
||||
"Test2, test2 <2@example.com>".parse().unwrap(),
|
||||
"\"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_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())));
|
||||
@@ -324,9 +340,20 @@ mod test {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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);
|
||||
}
|
||||
|
||||
#[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>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
pub fn get<H: Header>(&self) -> Option<H> {
|
||||
@@ -310,7 +310,7 @@ impl HeaderValue {
|
||||
/// acceptable for use if `encoded_value` contains only ascii
|
||||
/// 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(
|
||||
name: HeaderName,
|
||||
raw_value: String,
|
||||
@@ -474,7 +474,7 @@ mod tests {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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!(
|
||||
@@ -488,7 +488,7 @@ mod tests {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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!(
|
||||
@@ -506,7 +506,7 @@ mod tests {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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!(
|
||||
@@ -525,7 +525,7 @@ mod tests {
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string()
|
||||
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_owned()
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -543,7 +543,7 @@ mod tests {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string()
|
||||
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_owned()
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -557,7 +557,7 @@ mod tests {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"Seán <sean@example.com>".to_string(),
|
||||
"Seán <sean@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -571,7 +571,7 @@ mod tests {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"🌎 <world@example.com>".to_string(),
|
||||
"🌎 <world@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -609,7 +609,7 @@ mod tests {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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
|
||||
@@ -633,7 +633,7 @@ mod tests {
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(),)
|
||||
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_owned(),)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -654,7 +654,7 @@ mod tests {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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!(
|
||||
@@ -669,22 +669,22 @@ mod tests {
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
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(
|
||||
HeaderValue::new(
|
||||
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(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"Someone <somewhere@example.com>".to_string(),
|
||||
"Someone <somewhere@example.com>".to_owned(),
|
||||
));
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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
|
||||
@@ -712,7 +712,7 @@ mod tests {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjgggggggggfっ".to_string(),
|
||||
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjgggggggggfっ".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -91,14 +91,14 @@ mod test {
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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));
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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)));
|
||||
|
||||
@@ -125,7 +125,7 @@ mod test {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Sample subject".to_string(),
|
||||
"Sample subject".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod parsers;
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
mod types;
|
||||
|
||||
5
src/message/mailbox/parsers/mod.rs
Normal file
5
src/message/mailbox/parsers/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod rfc2234;
|
||||
mod rfc2822;
|
||||
mod rfc5336;
|
||||
|
||||
pub(crate) use rfc2822::{mailbox, mailbox_list};
|
||||
32
src/message/mailbox/parsers/rfc2234.rs
Normal file
32
src/message/mailbox/parsers/rfc2234.rs
Normal 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')))
|
||||
}
|
||||
248
src/message/mailbox/parsers/rfc2822.rs
Normal file
248
src/message/mailbox/parsers/rfc2822.rs
Normal 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())
|
||||
}
|
||||
17
src/message/mailbox/parsers/rfc5336.rs
Normal file
17
src/message/mailbox/parsers/rfc5336.rs
Normal 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)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ impl Serialize for Mailbox {
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
serializer.collect_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ impl Serialize for Mailboxes {
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
serializer.collect_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ mod 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();
|
||||
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();
|
||||
assert_eq!(
|
||||
m,
|
||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
@@ -211,7 +211,7 @@ mod test {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m,
|
||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
@@ -5,15 +5,17 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use chumsky::prelude::*;
|
||||
use email_encoding::headers::EmailWriter;
|
||||
|
||||
use super::parsers;
|
||||
use crate::address::{Address, AddressError};
|
||||
|
||||
/// 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_).
|
||||
///
|
||||
/// **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
|
||||
///
|
||||
@@ -86,7 +88,7 @@ impl Mailbox {
|
||||
|
||||
impl Display for Mailbox {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
if let Some(ref name) = self.name {
|
||||
if let Some(name) = &self.name {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() {
|
||||
write_word(f, name)?;
|
||||
@@ -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 {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
||||
match (src.find('<'), src.find('>')) {
|
||||
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => {
|
||||
let name = src.split_at(addr_open).0;
|
||||
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 name = name.trim();
|
||||
let name = if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.into())
|
||||
};
|
||||
Ok(Mailbox::new(name, addr))
|
||||
}
|
||||
(Some(_), _) => Err(AddressError::Unbalanced),
|
||||
_ => {
|
||||
let addr = src.parse()?;
|
||||
Ok(Mailbox::new(None, addr))
|
||||
}
|
||||
}
|
||||
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
|
||||
let mailbox = Mailbox::new(name, Address::new(user, domain)?);
|
||||
|
||||
Ok(mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Address> for Mailbox {
|
||||
fn from(value: Address) -> Self {
|
||||
Self::new(None, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ..._).
|
||||
///
|
||||
/// **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)]
|
||||
pub struct Mailboxes(Vec<Mailbox>);
|
||||
|
||||
@@ -356,34 +342,16 @@ impl Display for Mailboxes {
|
||||
impl FromStr for Mailboxes {
|
||||
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();
|
||||
|
||||
if !src.is_empty() {
|
||||
// n-1 elements
|
||||
let mut skip = 0;
|
||||
while let Some(i) = src[skip..].find(',') {
|
||||
let left = &src[..skip + i];
|
||||
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
|
||||
match left.trim().parse() {
|
||||
Ok(mailbox) => {
|
||||
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);
|
||||
for (name, (user, domain)) in parsed_mailboxes {
|
||||
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
|
||||
}
|
||||
|
||||
Ok(Mailboxes(mailboxes))
|
||||
@@ -436,7 +404,7 @@ fn is_valid_atom_char(c: u8) -> bool {
|
||||
b'}' |
|
||||
b'~' |
|
||||
|
||||
// Not techically allowed but will be escaped into allowed characters.
|
||||
// Not technically allowed but will be escaped into allowed characters.
|
||||
128..=255)
|
||||
}
|
||||
|
||||
@@ -620,7 +588,7 @@ mod test {
|
||||
#[test]
|
||||
fn parse_address_from_tuple() {
|
||||
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(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
|
||||
@@ -17,6 +17,16 @@ pub(super) enum Part {
|
||||
Multi(MultiPart),
|
||||
}
|
||||
|
||||
impl Part {
|
||||
#[cfg(feature = "dkim")]
|
||||
pub(super) fn format_body(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
Part::Single(part) => part.format_body(out),
|
||||
Part::Multi(part) => part.format_body(out),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for Part {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
@@ -100,14 +110,14 @@ impl SinglePart {
|
||||
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 {
|
||||
Self::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.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 {
|
||||
Self::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
@@ -132,6 +142,12 @@ impl SinglePart {
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Format only the signlepart body
|
||||
fn format_body(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.body);
|
||||
out.extend_from_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for SinglePart {
|
||||
@@ -139,8 +155,7 @@ impl EmailFormat for SinglePart {
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out.extend_from_slice(&self.body);
|
||||
out.extend_from_slice(b"\r\n");
|
||||
self.format_body(out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,17 +164,17 @@ impl EmailFormat for SinglePart {
|
||||
pub enum MultiPartKind {
|
||||
/// 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,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// Encrypted kind for encrypted messages
|
||||
@@ -373,14 +388,9 @@ impl MultiPart {
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for MultiPart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
out.extend_from_slice(b"\r\n");
|
||||
|
||||
/// Format only the multipart body
|
||||
fn format_body(&self, out: &mut Vec<u8>) {
|
||||
let boundary = self.boundary();
|
||||
|
||||
for part in &self.parts {
|
||||
@@ -396,6 +406,15 @@ impl EmailFormat for MultiPart {
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for MultiPart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
out.extend_from_slice(b"\r\n");
|
||||
self.format_body(out);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -345,9 +345,9 @@ impl MessageBuilder {
|
||||
let hostname = hostname::get()
|
||||
.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"))]
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
|
||||
|
||||
self.header(header::MessageId::from(
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6.4
|
||||
@@ -388,13 +388,13 @@ impl MessageBuilder {
|
||||
|
||||
/// 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,
|
||||
/// like when saving the email as an `.eml`, or sending through
|
||||
/// some transports (like the Gmail API) that don't take a separate
|
||||
/// 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 {
|
||||
self.drop_bcc = false;
|
||||
self
|
||||
@@ -503,6 +503,11 @@ impl Message {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the headers
|
||||
pub fn headers_mut(&mut self) -> &mut Headers {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Get `Message` envelope
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
@@ -520,7 +525,7 @@ impl Message {
|
||||
pub(crate) fn body_raw(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
match &self.body {
|
||||
MessageBody::Mime(p) => p.format(&mut out),
|
||||
MessageBody::Mime(p) => p.format_body(&mut out),
|
||||
MessageBody::Raw(r) => out.extend_from_slice(r),
|
||||
};
|
||||
out.extend_from_slice(b"\r\n");
|
||||
@@ -541,7 +546,7 @@ impl Message {
|
||||
/// .reply_to("Bob <bob@example.org>".parse().unwrap())
|
||||
/// .to("Carla <carla@example.net>".parse().unwrap())
|
||||
/// .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();
|
||||
/// let key = "-----BEGIN RSA PRIVATE KEY-----
|
||||
/// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
|
||||
@@ -572,8 +577,8 @@ impl Message {
|
||||
/// -----END RSA PRIVATE KEY-----";
|
||||
/// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
|
||||
/// message.sign(&DkimConfig::default_config(
|
||||
/// "dkimtest".to_string(),
|
||||
/// "example.org".to_string(),
|
||||
/// "dkimtest".to_owned(),
|
||||
/// "example.org".to_owned(),
|
||||
/// signing_key,
|
||||
/// ));
|
||||
/// println!(
|
||||
@@ -630,7 +635,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_miminal_message() {
|
||||
fn email_minimal_message() {
|
||||
assert!(Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.to("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
|
||||
@@ -54,7 +54,7 @@ impl fmt::Debug for Error {
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(ref source) = self.inner.source {
|
||||
if let Some(source) = &self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ impl fmt::Display for Error {
|
||||
Kind::Envelope => f.write_str("internal client error")?,
|
||||
};
|
||||
|
||||
if let Some(ref e) = self.inner.source {
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ mod error;
|
||||
type Id = String;
|
||||
|
||||
/// Writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
||||
pub struct FileTransport {
|
||||
@@ -167,7 +167,7 @@ pub struct FileTransport {
|
||||
}
|
||||
|
||||
/// Asynchronously writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
|
||||
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
|
||||
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
|
||||
//! | [`stub`] | Debug | [`StubTransport`] | [`StubTransport`] | Drops the email - Useful for debugging |
|
||||
//! | [`stub`] | Debug | [`StubTransport`] | [`AsyncStubTransport`] | Drops the email - Useful for debugging |
|
||||
//!
|
||||
//! ## Building an email
|
||||
//!
|
||||
@@ -65,7 +65,7 @@
|
||||
//! .subject("Happy new year")
|
||||
//! .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
|
||||
//! let mailer = SmtpTransport::relay("smtp.gmail.com")?
|
||||
@@ -75,7 +75,7 @@
|
||||
//! // Send the email
|
||||
//! match mailer.send(&email) {
|
||||
//! Ok(_) => println!("Email sent successfully!"),
|
||||
//! Err(e) => panic!("Could not send email: {:?}", e),
|
||||
//! Err(e) => panic!("Could not send email: {e:?}"),
|
||||
//! }
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
@@ -97,6 +97,7 @@
|
||||
//! [`FileTransport`]: crate::FileTransport
|
||||
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
//! [`StubTransport`]: crate::transport::stub::StubTransport
|
||||
//! [`AsyncStubTransport`]: crate::transport::stub::AsyncStubTransport
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
@@ -52,7 +52,7 @@ impl fmt::Debug for Error {
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(ref source) = self.inner.source {
|
||||
if let Some(source) = &self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ impl fmt::Display for Error {
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
};
|
||||
|
||||
if let Some(ref e) = self.inner.source {
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ where
|
||||
.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
|
||||
/// that don't take SMTPS connections.
|
||||
@@ -151,28 +151,114 @@ where
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
/// * A 60-seconds timeout for smtp commands
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
|
||||
let info = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
AsyncSmtpTransportBuilder {
|
||||
info,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
AsyncSmtpTransportBuilder::new(server)
|
||||
}
|
||||
|
||||
/// Creates a `AsyncSmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// The protocol, credentials, host and port can be provided in a single URL.
|
||||
/// 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,
|
||||
/// 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
|
||||
///
|
||||
/// `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> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
@@ -219,6 +305,20 @@ pub struct AsyncSmtpTransportBuilder {
|
||||
|
||||
/// Builder for the SMTP `AsyncSmtpTransport`
|
||||
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
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
|
||||
@@ -51,7 +51,7 @@ pub enum Mechanism {
|
||||
/// [RFC 4616](https://tools.ietf.org/html/rfc4616)
|
||||
Plain,
|
||||
/// 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).
|
||||
Login,
|
||||
@@ -71,7 +71,7 @@ impl Display for Mechanism {
|
||||
}
|
||||
|
||||
impl Mechanism {
|
||||
/// Does the mechanism supports initial response
|
||||
/// Does the mechanism support initial response?
|
||||
pub fn supports_initial_response(self) -> bool {
|
||||
match self {
|
||||
Mechanism::Plain | Mechanism::Xoauth2 => true,
|
||||
@@ -98,11 +98,13 @@ impl Mechanism {
|
||||
let decoded_challenge = challenge
|
||||
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
|
||||
|
||||
if ["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
||||
if ["User Name", "Username:", "Username", "User Name\0"]
|
||||
.contains(&decoded_challenge)
|
||||
{
|
||||
return Ok(credentials.authentication_identity.clone());
|
||||
}
|
||||
|
||||
if ["Password", "Password:"].contains(&decoded_challenge) {
|
||||
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.secret.clone());
|
||||
}
|
||||
|
||||
@@ -127,7 +129,7 @@ mod test {
|
||||
fn test_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!(
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
@@ -140,7 +142,7 @@ mod test {
|
||||
fn test_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!(
|
||||
mechanism.response(&credentials, Some("Username")).unwrap(),
|
||||
@@ -158,8 +160,8 @@ mod test {
|
||||
let mechanism = Mechanism::Xoauth2;
|
||||
|
||||
let credentials = Credentials::new(
|
||||
"username".to_string(),
|
||||
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
|
||||
"username".to_owned(),
|
||||
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -172,7 +174,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_from_user_pass_for_credentials() {
|
||||
assert_eq!(
|
||||
Credentials::new("alice".to_string(), "wonderland".to_string()),
|
||||
Credentials::new("alice".to_owned(), "wonderland".to_owned()),
|
||||
Credentials::from(("alice", "wonderland"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use super::async_net::AsyncTokioStream;
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||
use super::{AsyncNetworkStream, ClientCodec, ConnectionState, TlsParameters};
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
@@ -19,25 +19,11 @@ use crate::{
|
||||
Envelope,
|
||||
};
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
$client.abort().await;
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct AsyncSmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: BufReader<AsyncNetworkStream>,
|
||||
/// Panic state
|
||||
panic: bool,
|
||||
/// Information about the server
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
@@ -62,7 +48,35 @@ impl AsyncSmtpConnection {
|
||||
|
||||
/// 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_address` 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
|
||||
///
|
||||
/// # 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")]
|
||||
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
|
||||
server: T,
|
||||
@@ -98,7 +112,6 @@ impl AsyncSmtpConnection {
|
||||
let stream = BufReader::new(stream);
|
||||
let mut conn = AsyncSmtpConnection {
|
||||
stream,
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
// TODO log
|
||||
@@ -132,7 +145,7 @@ impl AsyncSmtpConnection {
|
||||
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 !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
return Err(error::client(
|
||||
@@ -142,50 +155,54 @@ impl AsyncSmtpConnection {
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options))
|
||||
.await,
|
||||
self
|
||||
);
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options))
|
||||
.await?;
|
||||
|
||||
// Recipient
|
||||
for to_address in envelope.to() {
|
||||
try_smtp!(
|
||||
self.command(Rcpt::new(to_address.clone(), vec![])).await,
|
||||
self
|
||||
);
|
||||
self.command(Rcpt::new(to_address.clone(), vec![])).await?;
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.command(Data).await, self);
|
||||
self.command(Data).await?;
|
||||
|
||||
// Message content
|
||||
let result = try_smtp!(self.message(email).await, self);
|
||||
Ok(result)
|
||||
self.message(email).await
|
||||
}
|
||||
|
||||
pub fn has_broken(&self) -> bool {
|
||||
self.panic
|
||||
match self.stream.get_ref().state() {
|
||||
ConnectionState::Ok => false,
|
||||
ConnectionState::Broken | ConnectionState::Closed => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_starttls(&self) -> bool {
|
||||
!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)]
|
||||
pub async fn starttls(
|
||||
&mut self,
|
||||
mut self,
|
||||
tls_parameters: TlsParameters,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Self, Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
try_smtp!(self.command(Starttls).await, self);
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters).await?;
|
||||
self.command(Starttls).await?;
|
||||
let stream = self.stream.into_inner();
|
||||
let stream = stream.upgrade_tls(tls_parameters).await?;
|
||||
self.stream = BufReader::new(stream);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name).await, self);
|
||||
Ok(())
|
||||
self.ehlo(hello_name).await?;
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(error::client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
@@ -193,22 +210,24 @@ impl AsyncSmtpConnection {
|
||||
|
||||
/// Send EHLO and update server info
|
||||
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
|
||||
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self);
|
||||
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
|
||||
let ehlo_response = self.command(Ehlo::new(hello_name.clone())).await?;
|
||||
self.server_info = ServerInfo::from_response(&ehlo_response)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn quit(&mut self) -> Result<Response, Error> {
|
||||
Ok(try_smtp!(self.command(Quit).await, self))
|
||||
self.command(Quit).await
|
||||
}
|
||||
|
||||
pub async fn abort(&mut self) {
|
||||
// Only try to quit if we are not already broken
|
||||
if !self.panic {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit).await;
|
||||
match self.stream.get_ref().state() {
|
||||
ConnectionState::Ok | ConnectionState::Broken => {
|
||||
let _ = self.command(Quit).await;
|
||||
let _ = self.stream.close().await;
|
||||
self.stream.get_mut().set_state(ConnectionState::Closed);
|
||||
}
|
||||
ConnectionState::Closed => {}
|
||||
}
|
||||
let _ = self.stream.close().await;
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
@@ -226,7 +245,7 @@ impl AsyncSmtpConnection {
|
||||
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(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
@@ -245,15 +264,13 @@ impl AsyncSmtpConnection {
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = try_smtp!(
|
||||
self.command(Auth::new_from_response(
|
||||
response = self
|
||||
.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)
|
||||
.await,
|
||||
self
|
||||
);
|
||||
.await?;
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
@@ -281,6 +298,9 @@ impl AsyncSmtpConnection {
|
||||
|
||||
/// Writes a string to the server
|
||||
async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream.get_ref().state().verify()?;
|
||||
self.stream.get_mut().set_state(ConnectionState::Broken);
|
||||
|
||||
self.stream
|
||||
.get_mut()
|
||||
.write_all(string)
|
||||
@@ -292,6 +312,8 @@ impl AsyncSmtpConnection {
|
||||
.await
|
||||
.map_err(error::network)?;
|
||||
|
||||
self.stream.get_mut().set_state(ConnectionState::Ok);
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
Ok(())
|
||||
@@ -299,6 +321,9 @@ impl AsyncSmtpConnection {
|
||||
|
||||
/// Gets the SMTP response
|
||||
pub async fn read_response(&mut self) -> Result<Response, Error> {
|
||||
self.stream.get_ref().state().verify()?;
|
||||
self.stream.get_mut().set_state(ConnectionState::Broken);
|
||||
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self
|
||||
@@ -312,6 +337,8 @@ impl AsyncSmtpConnection {
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
self.stream.get_mut().set_state(ConnectionState::Ok);
|
||||
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
@@ -319,7 +346,7 @@ impl AsyncSmtpConnection {
|
||||
response.code(),
|
||||
Some(response.message().collect()),
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(error::response(e.to_string()));
|
||||
|
||||
@@ -11,11 +11,12 @@ use async_native_tls::TlsStream as AsyncStd1TlsStream;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
|
||||
use futures_io::{
|
||||
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
|
||||
Result as IoResult,
|
||||
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Result as IoResult,
|
||||
};
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
||||
#[cfg(any(feature = "tokio1-rustls-tls", feature = "async-std1-rustls-tls"))]
|
||||
use rustls::pki_types::ServerName;
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
use tokio1_boring::SslStream as Tokio1SslStream;
|
||||
#[cfg(feature = "tokio1")]
|
||||
@@ -38,7 +39,7 @@ use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
use super::{ConnectionState, TlsParameters};
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::transport::smtp::client::net::resolved_address_filter;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
@@ -47,6 +48,7 @@ use crate::transport::smtp::{error, Error};
|
||||
#[derive(Debug)]
|
||||
pub struct AsyncNetworkStream {
|
||||
inner: InnerAsyncNetworkStream,
|
||||
state: ConnectionState,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
@@ -89,45 +91,43 @@ enum InnerAsyncNetworkStream {
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
|
||||
impl AsyncNetworkStream {
|
||||
fn new(inner: InnerAsyncNetworkStream) -> Self {
|
||||
if let InnerAsyncNetworkStream::None = inner {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
AsyncNetworkStream {
|
||||
inner,
|
||||
state: ConnectionState::Ok,
|
||||
}
|
||||
}
|
||||
|
||||
AsyncNetworkStream { inner }
|
||||
pub(super) fn state(&self) -> ConnectionState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub(super) fn set_state(&mut self, state: ConnectionState) {
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
|
||||
match self.inner {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(),
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref s) => {
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
|
||||
s.get_ref().get_ref().get_ref().peer_addr()
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Err(IoError::new(
|
||||
ErrorKind::Other,
|
||||
"InnerAsyncNetworkStream::None must never be built",
|
||||
))
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ impl AsyncNetworkStream {
|
||||
let mut stream =
|
||||
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
stream = stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
@@ -208,9 +208,9 @@ impl AsyncNetworkStream {
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> 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
|
||||
// connected which is a blocking operation.
|
||||
// been connected, which is a blocking operation.
|
||||
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Duration,
|
||||
@@ -248,13 +248,13 @@ impl AsyncNetworkStream {
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
stream = stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
pub async fn upgrade_tls(self, tls_parameters: TlsParameters) -> Result<Self, Error> {
|
||||
match self.inner {
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
not(any(
|
||||
@@ -273,18 +273,14 @@ impl AsyncNetworkStream {
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "tokio1-boring-tls"
|
||||
))]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => {
|
||||
let inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
Ok(Self {
|
||||
inner,
|
||||
state: ConnectionState::Ok,
|
||||
})
|
||||
}
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
@@ -296,20 +292,16 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => {
|
||||
let inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
Ok(Self {
|
||||
inner,
|
||||
state: ConnectionState::Ok,
|
||||
})
|
||||
}
|
||||
_ => Ok(()),
|
||||
_ => Ok(self),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +315,7 @@ impl AsyncNetworkStream {
|
||||
tcp_stream: Box<dyn AsyncTokioStream>,
|
||||
tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = tls_parameters.domain().to_string();
|
||||
let domain = tls_parameters.domain().to_owned();
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
@@ -350,7 +342,6 @@ impl AsyncNetworkStream {
|
||||
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
return {
|
||||
use rustls::ServerName;
|
||||
use tokio1_rustls::TlsConnector;
|
||||
|
||||
let domain = ServerName::try_from(domain.as_str())
|
||||
@@ -358,7 +349,7 @@ impl AsyncNetworkStream {
|
||||
|
||||
let connector = TlsConnector::from(config);
|
||||
let stream = connector
|
||||
.connect(domain, tcp_stream)
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
||||
@@ -424,14 +415,13 @@ impl AsyncNetworkStream {
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
return {
|
||||
use futures_rustls::TlsConnector;
|
||||
use rustls::ServerName;
|
||||
|
||||
let domain = ServerName::try_from(domain.as_str())
|
||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||
|
||||
let connector = TlsConnector::from(config);
|
||||
let stream = connector
|
||||
.connect(domain, tcp_stream)
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
||||
@@ -445,7 +435,7 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.inner {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
@@ -460,7 +450,6 @@ impl AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
|
||||
InnerAsyncNetworkStream::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,8 +475,7 @@ impl AsyncNetworkStream {
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.0),
|
||||
.to_vec()),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
||||
.ssl()
|
||||
@@ -509,9 +497,7 @@ impl AsyncNetworkStream {
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.0),
|
||||
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
.to_vec()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -522,9 +508,9 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match self.inner {
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
@@ -533,7 +519,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => {
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
@@ -542,7 +528,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => {
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
@@ -551,7 +537,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => {
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
@@ -560,19 +546,11 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
||||
Pin::new(s).poll_read(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
||||
Pin::new(s).poll_read(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -583,75 +561,61 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match self.inner {
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
||||
Pin::new(s).poll_write(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
||||
Pin::new(s).poll_write(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match self.inner {
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match self.inner {
|
||||
self.state = ConnectionState::Closed;
|
||||
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
use super::{ClientCodec, NetworkStream, TlsParameters};
|
||||
use super::{ClientCodec, ConnectionState, NetworkStream, TlsParameters};
|
||||
use crate::{
|
||||
address::Envelope,
|
||||
transport::smtp::{
|
||||
@@ -20,25 +20,11 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
$client.abort();
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct SmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: BufReader<NetworkStream>,
|
||||
/// Panic state
|
||||
panic: bool,
|
||||
/// Information about the server
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
@@ -65,7 +51,6 @@ impl SmtpConnection {
|
||||
let stream = BufReader::new(stream);
|
||||
let mut conn = SmtpConnection {
|
||||
stream,
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
conn.set_timeout(timeout).map_err(error::network)?;
|
||||
@@ -100,7 +85,7 @@ impl SmtpConnection {
|
||||
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 !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
return Err(error::client(
|
||||
@@ -110,26 +95,25 @@ impl SmtpConnection {
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options)),
|
||||
self
|
||||
);
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options))?;
|
||||
|
||||
// Recipient
|
||||
for to_address in envelope.to() {
|
||||
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
|
||||
self.command(Rcpt::new(to_address.clone(), vec![]))?;
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.command(Data), self);
|
||||
self.command(Data)?;
|
||||
|
||||
// Message content
|
||||
let result = try_smtp!(self.message(email), self);
|
||||
Ok(result)
|
||||
self.message(email)
|
||||
}
|
||||
|
||||
pub fn has_broken(&self) -> bool {
|
||||
self.panic
|
||||
match self.stream.get_ref().state() {
|
||||
ConnectionState::Ok => false,
|
||||
ConnectionState::Broken | ConnectionState::Closed => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_starttls(&self) -> bool {
|
||||
@@ -138,20 +122,22 @@ impl SmtpConnection {
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn starttls(
|
||||
&mut self,
|
||||
mut self,
|
||||
tls_parameters: &TlsParameters,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Self, Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
{
|
||||
try_smtp!(self.command(Starttls), self);
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters)?;
|
||||
self.command(Starttls)?;
|
||||
let stream = self.stream.into_inner();
|
||||
let stream = stream.upgrade_tls(tls_parameters)?;
|
||||
self.stream = BufReader::new(stream);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name), self);
|
||||
Ok(())
|
||||
self.ehlo(hello_name)?;
|
||||
Ok(self)
|
||||
}
|
||||
#[cfg(not(any(
|
||||
feature = "native-tls",
|
||||
@@ -168,22 +154,24 @@ impl SmtpConnection {
|
||||
|
||||
/// Send EHLO and update server info
|
||||
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
|
||||
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self);
|
||||
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
|
||||
let ehlo_response = self.command(Ehlo::new(hello_name.clone()))?;
|
||||
self.server_info = ServerInfo::from_response(&ehlo_response)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) -> Result<Response, Error> {
|
||||
Ok(try_smtp!(self.command(Quit), self))
|
||||
self.command(Quit)
|
||||
}
|
||||
|
||||
pub fn abort(&mut self) {
|
||||
// Only try to quit if we are not already broken
|
||||
if !self.panic {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit);
|
||||
match self.stream.get_ref().state() {
|
||||
ConnectionState::Ok | ConnectionState::Broken => {
|
||||
let _ = self.command(Quit);
|
||||
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
|
||||
self.stream.get_mut().set_state(ConnectionState::Closed);
|
||||
}
|
||||
ConnectionState::Closed => {}
|
||||
}
|
||||
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
@@ -207,7 +195,7 @@ impl SmtpConnection {
|
||||
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(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
@@ -224,14 +212,11 @@ impl SmtpConnection {
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = try_smtp!(
|
||||
self.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?),
|
||||
self
|
||||
);
|
||||
response = self.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)?;
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
@@ -260,12 +245,17 @@ impl SmtpConnection {
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream.get_ref().state().verify()?;
|
||||
self.stream.get_mut().set_state(ConnectionState::Broken);
|
||||
|
||||
self.stream
|
||||
.get_mut()
|
||||
.write_all(string)
|
||||
.map_err(error::network)?;
|
||||
self.stream.get_mut().flush().map_err(error::network)?;
|
||||
|
||||
self.stream.get_mut().set_state(ConnectionState::Ok);
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
Ok(())
|
||||
@@ -273,6 +263,9 @@ impl SmtpConnection {
|
||||
|
||||
/// Gets the SMTP response
|
||||
pub fn read_response(&mut self) -> Result<Response, Error> {
|
||||
self.stream.get_ref().state().verify()?;
|
||||
self.stream.get_mut().set_state(ConnectionState::Broken);
|
||||
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
|
||||
@@ -280,6 +273,8 @@ impl SmtpConnection {
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
self.stream.get_mut().set_state(ConnectionState::Ok);
|
||||
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! 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)?;
|
||||
//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
|
||||
//! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
|
||||
@@ -38,8 +38,9 @@ pub(super) use self::tls::InnerTlsParameters;
|
||||
pub use self::tls::TlsVersion;
|
||||
pub use self::{
|
||||
connection::SmtpConnection,
|
||||
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
|
||||
tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
|
||||
};
|
||||
use super::{error, Error};
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_connection;
|
||||
@@ -49,6 +50,23 @@ mod connection;
|
||||
mod net;
|
||||
mod tls;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum ConnectionState {
|
||||
Ok,
|
||||
Broken,
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl ConnectionState {
|
||||
fn verify(&mut self) -> Result<(), Error> {
|
||||
match self {
|
||||
Self::Ok => Ok(()),
|
||||
Self::Broken => Err(error::connection("connection broken")),
|
||||
Self::Closed => Err(error::connection("connection closed")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The codec used for transparency
|
||||
#[derive(Debug)]
|
||||
struct ClientCodec {
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
mem,
|
||||
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
||||
net::{IpAddr, Shutdown, SocketAddr, TcpStream, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -12,17 +11,18 @@ use boring::ssl::SslStream;
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::TlsStream;
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::{ClientConnection, ServerName, StreamOwned};
|
||||
use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
|
||||
use socket2::{Domain, Protocol, Type};
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
use super::{ConnectionState, TlsParameters};
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// A network stream
|
||||
pub struct NetworkStream {
|
||||
inner: InnerNetworkStream,
|
||||
state: ConnectionState,
|
||||
}
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
@@ -40,53 +40,49 @@ enum InnerNetworkStream {
|
||||
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
BoringTls(SslStream<TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
|
||||
impl NetworkStream {
|
||||
fn new(inner: InnerNetworkStream) -> Self {
|
||||
if let InnerNetworkStream::None = inner {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
NetworkStream {
|
||||
inner,
|
||||
state: ConnectionState::Ok,
|
||||
}
|
||||
}
|
||||
|
||||
NetworkStream { inner }
|
||||
pub(super) fn state(&self) -> ConnectionState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub(super) fn set_state(&mut self, state: ConnectionState) {
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref s) => s.peer_addr(),
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::RustlsTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
80,
|
||||
)))
|
||||
}
|
||||
InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdowns the connection
|
||||
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
|
||||
pub fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
|
||||
self.state = ConnectionState::Closed;
|
||||
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.shutdown(how),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::RustlsTls(s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(ref s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,13 +135,13 @@ impl NetworkStream {
|
||||
let tcp_stream = try_connect(server, timeout, local_addr)?;
|
||||
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters)?;
|
||||
stream = stream.upgrade_tls(tls_parameters)?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
pub fn upgrade_tls(self, tls_parameters: &TlsParameters) -> Result<Self, Error> {
|
||||
match self.inner {
|
||||
#[cfg(not(any(
|
||||
feature = "native-tls",
|
||||
feature = "rustls-tls",
|
||||
@@ -157,18 +153,14 @@ impl NetworkStream {
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
|
||||
Ok(())
|
||||
InnerNetworkStream::Tcp(tcp_stream) => {
|
||||
let inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
|
||||
Ok(Self {
|
||||
inner,
|
||||
state: ConnectionState::Ok,
|
||||
})
|
||||
}
|
||||
_ => Ok(()),
|
||||
_ => Ok(self),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +181,7 @@ impl NetworkStream {
|
||||
InnerTlsParameters::RustlsTls(connector) => {
|
||||
let domain = ServerName::try_from(tls_parameters.domain())
|
||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||
let connection = ClientConnection::new(Arc::clone(connector), domain)
|
||||
let connection = ClientConnection::new(Arc::clone(connector), domain.to_owned())
|
||||
.map_err(error::connection)?;
|
||||
let stream = StreamOwned::new(connection, tcp_stream);
|
||||
InnerNetworkStream::RustlsTls(stream)
|
||||
@@ -208,7 +200,7 @@ impl NetworkStream {
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.inner {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(_) => false,
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => true,
|
||||
@@ -216,10 +208,6 @@ impl NetworkStream {
|
||||
InnerNetworkStream::RustlsTls(_) => true,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(_) => true,
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,8 +229,7 @@ impl NetworkStream {
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.0),
|
||||
.to_vec()),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => Ok(stream
|
||||
.ssl()
|
||||
@@ -250,111 +237,78 @@ impl NetworkStream {
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set write timeout for IO calls
|
||||
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(stream) => stream.set_write_timeout(duration),
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(ref mut stream) => {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for NetworkStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.read(buf),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.read(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
|
||||
InnerNetworkStream::NativeTls(s) => s.read(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
|
||||
InnerNetworkStream::RustlsTls(s) => s.read(buf),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(ref mut s) => s.read(buf),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(0)
|
||||
}
|
||||
InnerNetworkStream::BoringTls(s) => s.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for NetworkStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.write(buf),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.write(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
|
||||
InnerNetworkStream::NativeTls(s) => s.write(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
|
||||
InnerNetworkStream::RustlsTls(s) => s.write(buf),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(ref mut s) => s.write(buf),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(0)
|
||||
}
|
||||
InnerNetworkStream::BoringTls(s) => s.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.flush(),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.flush(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
|
||||
InnerNetworkStream::NativeTls(s) => s.flush(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
|
||||
InnerNetworkStream::RustlsTls(s) => s.flush(),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(ref mut s) => s.flush(),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
InnerNetworkStream::BoringTls(s) => s.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// See: https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560
|
||||
fn bind_local_address(
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
use std::fmt::{self, Debug};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
use std::{io, sync::Arc};
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
use boring::ssl::{SslConnector, SslVersion};
|
||||
use boring::{
|
||||
ssl::{SslConnector, SslVersion},
|
||||
x509::store::X509StoreBuilder,
|
||||
};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::{
|
||||
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
|
||||
ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName,
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
crypto::{verify_tls12_signature, verify_tls13_signature},
|
||||
pki_types::{CertificateDer, ServerName, UnixTime},
|
||||
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
@@ -44,9 +49,9 @@ pub enum TlsVersion {
|
||||
Tlsv12,
|
||||
/// 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.
|
||||
Tlsv13,
|
||||
}
|
||||
@@ -94,6 +99,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
|
||||
#[derive(Clone)]
|
||||
pub struct TlsParameters {
|
||||
@@ -108,6 +137,7 @@ pub struct TlsParameters {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TlsParametersBuilder {
|
||||
domain: String,
|
||||
cert_store: CertificateStore,
|
||||
root_certs: Vec<Certificate>,
|
||||
accept_invalid_hostnames: bool,
|
||||
accept_invalid_certs: bool,
|
||||
@@ -120,6 +150,7 @@ impl TlsParametersBuilder {
|
||||
pub fn new(domain: String) -> Self {
|
||||
Self {
|
||||
domain,
|
||||
cert_store: CertificateStore::Default,
|
||||
root_certs: Vec::new(),
|
||||
accept_invalid_hostnames: false,
|
||||
accept_invalid_certs: false,
|
||||
@@ -128,6 +159,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
|
||||
///
|
||||
/// Can be used to safely connect to a server using a self signed certificate, for example.
|
||||
@@ -208,6 +245,18 @@ impl TlsParametersBuilder {
|
||||
pub fn build_native(self) -> Result<TlsParameters, Error> {
|
||||
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 {
|
||||
tls_builder.add_root_certificate(cert.native_tls);
|
||||
}
|
||||
@@ -246,6 +295,21 @@ impl TlsParametersBuilder {
|
||||
if self.accept_invalid_certs {
|
||||
tls_builder.set_verify(SslVerifyMode::NONE);
|
||||
} 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();
|
||||
|
||||
for cert in self.root_certs {
|
||||
@@ -275,8 +339,6 @@ impl TlsParametersBuilder {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||
let tls = ClientConfig::builder();
|
||||
|
||||
let just_version3 = &[&rustls::version::TLS13];
|
||||
let supported_versions = match self.min_tls_version {
|
||||
TlsVersion::Tlsv10 => {
|
||||
@@ -289,35 +351,50 @@ impl TlsParametersBuilder {
|
||||
TlsVersion::Tlsv13 => just_version3,
|
||||
};
|
||||
|
||||
let tls = tls
|
||||
.with_safe_default_cipher_suites()
|
||||
.with_safe_default_kx_groups()
|
||||
.with_protocol_versions(supported_versions)
|
||||
.map_err(error::tls)?;
|
||||
let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
|
||||
|
||||
let tls = if self.accept_invalid_certs {
|
||||
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
|
||||
tls.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
|
||||
} else {
|
||||
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 (added, ignored) = store.add_parsable_certificates(native_certs);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(
|
||||
"loaded platform certs with {added} valid and {ignored} ignored (invalid) certs"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
fn load_webpki_roots(store: &mut RootCertStore) {
|
||||
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
}
|
||||
|
||||
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 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(
|
||||
root_cert_store,
|
||||
None,
|
||||
)))
|
||||
tls.with_root_certificates(root_cert_store)
|
||||
};
|
||||
let tls = tls.with_no_client_auth();
|
||||
|
||||
@@ -391,7 +468,7 @@ pub struct Certificate {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls::Certificate,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: Vec<rustls::Certificate>,
|
||||
rustls: Vec<CertificateDer<'static>>,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: boring::x509::X509,
|
||||
}
|
||||
@@ -410,7 +487,7 @@ impl Certificate {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: vec![rustls::Certificate(der)],
|
||||
rustls: vec![der.into()],
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: boring_tls_cert,
|
||||
})
|
||||
@@ -430,10 +507,8 @@ impl Certificate {
|
||||
|
||||
let mut pem = Cursor::new(pem);
|
||||
rustls_pemfile::certs(&mut pem)
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
.map_err(|_| error::tls("invalid certificates"))?
|
||||
.into_iter()
|
||||
.map(rustls::Certificate)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
@@ -454,19 +529,53 @@ impl Debug for Certificate {
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[derive(Debug)]
|
||||
struct InvalidCertsVerifier;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::Certificate,
|
||||
_intermediates: &[rustls::Certificate],
|
||||
_server_name: &ServerName,
|
||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: SystemTime,
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, TlsError> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||
verify_tls12_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||
verify_tls13_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,15 +296,15 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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 mail_parameter = MailParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
keyword: "TEST".to_owned(),
|
||||
value: Some("value".to_owned()),
|
||||
};
|
||||
let rcpt_parameter = RcptParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
keyword: "TEST".to_owned(),
|
||||
value: Some("value".to_owned()),
|
||||
};
|
||||
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
|
||||
assert_eq!(
|
||||
@@ -346,19 +346,13 @@ mod test {
|
||||
assert_eq!(format!("{Noop}"), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", Help::new(Some("test".to_string()))),
|
||||
format!("{}", Help::new(Some("test".to_owned()))),
|
||||
"HELP test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Vrfy::new("test".to_string())),
|
||||
"VRFY test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Expn::new("test".to_string())),
|
||||
"EXPN test\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Vrfy::new("test".to_owned())), "VRFY test\r\n");
|
||||
assert_eq!(format!("{}", Expn::new("test".to_owned())), "EXPN test\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!(
|
||||
format!(
|
||||
"{}",
|
||||
|
||||
129
src/transport/smtp/connection_url.rs
Normal file
129
src/transport/smtp/connection_url.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
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 percent_decode = |s: &str| {
|
||||
percent_encoding::percent_decode_str(s)
|
||||
.decode_utf8()
|
||||
.map(|cow| cow.into_owned())
|
||||
.map_err(error::connection)
|
||||
};
|
||||
let credentials = Credentials::new(
|
||||
percent_decode(connection_url.username())?,
|
||||
percent_decode(password)?,
|
||||
);
|
||||
builder = builder.credentials(credentials);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
@@ -119,7 +119,7 @@ impl fmt::Debug for Error {
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(ref source) = self.inner.source {
|
||||
if let Some(source) = &self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
@@ -129,22 +129,22 @@ impl fmt::Debug for Error {
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.inner.kind {
|
||||
match &self.inner.kind {
|
||||
Kind::Response => f.write_str("response error")?,
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
Kind::Network => f.write_str("network error")?,
|
||||
Kind::Connection => f.write_str("Connection error")?,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
Kind::Tls => f.write_str("tls error")?,
|
||||
Kind::Transient(ref code) => {
|
||||
Kind::Transient(code) => {
|
||||
write!(f, "transient error ({code})")?;
|
||||
}
|
||||
Kind::Permanent(ref code) => {
|
||||
Kind::Permanent(code) => {
|
||||
write!(f, "permanent error ({code})")?;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref e) = self.inner.source {
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::{
|
||||
collections::HashSet,
|
||||
fmt::{self, Display, Formatter},
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
result::Result,
|
||||
};
|
||||
|
||||
use crate::transport::smtp::{
|
||||
@@ -53,10 +52,10 @@ impl Default for ClientId {
|
||||
|
||||
impl Display for ClientId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Domain(ref value) => f.write_str(value),
|
||||
Self::Ipv4(ref value) => write!(f, "[{value}]"),
|
||||
Self::Ipv6(ref value) => write!(f, "[IPv6:{value}]"),
|
||||
match self {
|
||||
Self::Domain(value) => f.write_str(value),
|
||||
Self::Ipv4(value) => write!(f, "[{value}]"),
|
||||
Self::Ipv6(value) => write!(f, "[IPv6:{value}]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,11 +92,11 @@ pub enum Extension {
|
||||
|
||||
impl Display for Extension {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
match self {
|
||||
Extension::EightBitMime => f.write_str("8BITMIME"),
|
||||
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
Extension::StartTls => f.write_str("STARTTLS"),
|
||||
Extension::Authentication(ref mechanism) => write!(f, "AUTH {mechanism}"),
|
||||
Extension::Authentication(mechanism) => write!(f, "AUTH {mechanism}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +118,7 @@ pub struct ServerInfo {
|
||||
impl Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let features = if self.features.is_empty() {
|
||||
"no supported features".to_string()
|
||||
"no supported features".to_owned()
|
||||
} else {
|
||||
format!("{:?}", self.features)
|
||||
};
|
||||
@@ -174,7 +173,7 @@ impl ServerInfo {
|
||||
}
|
||||
|
||||
Ok(ServerInfo {
|
||||
name: name.to_string(),
|
||||
name: name.to_owned(),
|
||||
features,
|
||||
})
|
||||
}
|
||||
@@ -190,7 +189,7 @@ impl ServerInfo {
|
||||
.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> {
|
||||
for mechanism in mechanisms {
|
||||
if self.supports_auth_mechanism(*mechanism) {
|
||||
@@ -227,16 +226,16 @@ pub enum MailParameter {
|
||||
|
||||
impl Display for MailParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
MailParameter::Body(ref value) => write!(f, "BODY={value}"),
|
||||
match self {
|
||||
MailParameter::Body(value) => write!(f, "BODY={value}"),
|
||||
MailParameter::Size(size) => write!(f, "SIZE={size}"),
|
||||
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
MailParameter::Other {
|
||||
ref keyword,
|
||||
value: Some(ref value),
|
||||
keyword,
|
||||
value: Some(value),
|
||||
} => write!(f, "{}={}", keyword, XText(value)),
|
||||
MailParameter::Other {
|
||||
ref keyword,
|
||||
keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
}
|
||||
@@ -277,13 +276,13 @@ pub enum RcptParameter {
|
||||
|
||||
impl Display for RcptParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
match &self {
|
||||
RcptParameter::Other {
|
||||
ref keyword,
|
||||
value: Some(ref value),
|
||||
} => write!(f, "{}={}", keyword, XText(value)),
|
||||
keyword,
|
||||
value: Some(value),
|
||||
} => write!(f, "{keyword}={}", XText(value)),
|
||||
RcptParameter::Other {
|
||||
ref keyword,
|
||||
keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
}
|
||||
@@ -304,21 +303,21 @@ mod test {
|
||||
#[test]
|
||||
fn test_clientid_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", ClientId::Domain("test".to_string())),
|
||||
"test".to_string()
|
||||
format!("{}", ClientId::Domain("test".to_owned())),
|
||||
"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]
|
||||
fn test_extension_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string()
|
||||
"8BITMIME".to_owned()
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_string()
|
||||
"AUTH PLAIN".to_owned()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -331,11 +330,11 @@ mod test {
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
name: "name".to_owned(),
|
||||
features: eightbitmime,
|
||||
}
|
||||
),
|
||||
"name with {EightBitMime}".to_string()
|
||||
"name with {EightBitMime}".to_owned()
|
||||
);
|
||||
|
||||
let empty = HashSet::new();
|
||||
@@ -344,11 +343,11 @@ mod test {
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
name: "name".to_owned(),
|
||||
features: empty,
|
||||
}
|
||||
),
|
||||
"name with no supported features".to_string()
|
||||
"name with no supported features".to_owned()
|
||||
);
|
||||
|
||||
let mut plain = HashSet::new();
|
||||
@@ -358,11 +357,11 @@ mod test {
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
name: "name".to_owned(),
|
||||
features: plain,
|
||||
}
|
||||
),
|
||||
"name with {Authentication(Plain)}".to_string()
|
||||
"name with {Authentication(Plain)}".to_owned()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -374,18 +373,14 @@ mod test {
|
||||
Category::Unspecified4,
|
||||
Detail::One,
|
||||
),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
|
||||
);
|
||||
|
||||
let mut features = HashSet::new();
|
||||
assert!(features.insert(Extension::EightBitMime));
|
||||
|
||||
let server_info = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
name: "me".to_owned(),
|
||||
features,
|
||||
};
|
||||
|
||||
@@ -401,10 +396,10 @@ mod test {
|
||||
Detail::One,
|
||||
),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"me".to_owned(),
|
||||
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
|
||||
"8BITMIME".to_owned(),
|
||||
"SIZE 42".to_owned(),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -414,7 +409,7 @@ mod test {
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
|
||||
|
||||
let server_info2 = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
name: "me".to_owned(),
|
||||
features: features2,
|
||||
};
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials(Credentials::new(
|
||||
//! "username".to_string(),
|
||||
//! "password".to_string(),
|
||||
//! "username".to_owned(),
|
||||
//! "password".to_owned(),
|
||||
//! ))
|
||||
//! // Configure expected authentication mechanism
|
||||
//! .authentication(vec![Mechanism::Plain])
|
||||
@@ -111,7 +111,7 @@
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // 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)
|
||||
//! .build()?;
|
||||
//!
|
||||
@@ -154,6 +154,7 @@ mod async_transport;
|
||||
pub mod authentication;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
mod connection_url;
|
||||
mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "pool")]
|
||||
@@ -200,7 +201,7 @@ struct SmtpInfo {
|
||||
impl Default for SmtpInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
server: "localhost".to_string(),
|
||||
server: "localhost".to_owned(),
|
||||
port: SMTP_PORT,
|
||||
hello_name: ClientId::default(),
|
||||
credentials: None,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{
|
||||
fmt::{self, Debug},
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::Arc,
|
||||
sync::{Arc, OnceLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ use futures_util::{
|
||||
lock::Mutex,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use super::{
|
||||
super::{client::AsyncSmtpConnection, Error},
|
||||
@@ -22,7 +21,7 @@ pub struct Pool<E: Executor> {
|
||||
config: PoolConfig,
|
||||
connections: Mutex<Vec<ParkedConnection>>,
|
||||
client: AsyncSmtpClient<E>,
|
||||
handle: OnceCell<E::Handle>,
|
||||
handle: OnceLock<E::Handle>,
|
||||
}
|
||||
|
||||
struct ParkedConnection {
|
||||
@@ -41,7 +40,7 @@ impl<E: Executor> Pool<E> {
|
||||
config,
|
||||
connections: Mutex::new(Vec::new()),
|
||||
client,
|
||||
handle: OnceCell::new(),
|
||||
handle: OnceLock::new(),
|
||||
});
|
||||
|
||||
{
|
||||
@@ -203,7 +202,7 @@ impl<E: Executor> Debug for Pool<E> {
|
||||
&match self.connections.try_lock() {
|
||||
Some(connections) => format!("{} connections", connections.len()),
|
||||
|
||||
None => "LOCKED".to_string(),
|
||||
None => "LOCKED".to_owned(),
|
||||
},
|
||||
)
|
||||
.field("client", &self.client)
|
||||
|
||||
@@ -186,8 +186,8 @@ impl Debug for Pool {
|
||||
&match self.connections.try_lock() {
|
||||
Ok(connections) => format!("{} connections", connections.len()),
|
||||
|
||||
Err(TryLockError::WouldBlock) => "LOCKED".to_string(),
|
||||
Err(TryLockError::Poisoned(_)) => "POISONED".to_string(),
|
||||
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
|
||||
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
|
||||
},
|
||||
)
|
||||
.field("client", &self.client)
|
||||
|
||||
@@ -5,7 +5,6 @@ use std::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
result,
|
||||
str::FromStr,
|
||||
string::ToString,
|
||||
};
|
||||
|
||||
use nom::{
|
||||
@@ -19,7 +18,7 @@ use nom::{
|
||||
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// First digit indicates severity
|
||||
/// The first digit indicates severity
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Severity {
|
||||
@@ -132,6 +131,12 @@ impl Code {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Code> for u16 {
|
||||
fn from(code: Code) -> Self {
|
||||
code.detail as u16 + 10 * code.category as u16 + 100 * code.severity as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an SMTP reply, with separated code and message
|
||||
///
|
||||
/// The text message is optional, only the code is mandatory
|
||||
@@ -151,7 +156,7 @@ impl FromStr for Response {
|
||||
fn from_str(s: &str) -> result::Result<Response, Error> {
|
||||
parse_response(s)
|
||||
.map(|(_, r)| r)
|
||||
.map_err(|e| error::response(e.to_string()))
|
||||
.map_err(|e| error::response(e.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +322,17 @@ mod test {
|
||||
assert_eq!(code.to_string(), "421");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_to_u16() {
|
||||
let code = Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: Detail::One,
|
||||
};
|
||||
let c: u16 = code.into();
|
||||
assert_eq!(c, 421);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_from_str() {
|
||||
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
|
||||
@@ -329,10 +345,10 @@ mod test {
|
||||
detail: Detail::Zero,
|
||||
},
|
||||
message: vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5".to_string(),
|
||||
"me".to_owned(),
|
||||
"8BITMIME".to_owned(),
|
||||
"SIZE 42".to_owned(),
|
||||
"AUTH PLAIN CRAM-MD5".to_owned(),
|
||||
],
|
||||
}
|
||||
);
|
||||
@@ -352,11 +368,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.is_positive());
|
||||
assert!(!Response::new(
|
||||
@@ -365,11 +377,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.is_positive());
|
||||
}
|
||||
@@ -382,11 +390,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.has_code(451));
|
||||
assert!(!Response::new(
|
||||
@@ -395,11 +399,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.has_code(251));
|
||||
}
|
||||
@@ -413,11 +413,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.first_word(),
|
||||
Some("me")
|
||||
@@ -430,9 +426,9 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me mo".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"me mo".to_owned(),
|
||||
"8BITMIME".to_owned(),
|
||||
"SIZE 42".to_owned(),
|
||||
],
|
||||
)
|
||||
.first_word(),
|
||||
@@ -457,7 +453,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
vec![" ".to_owned()],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
@@ -469,7 +465,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
vec![" ".to_owned()],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
@@ -481,7 +477,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec!["".to_string()],
|
||||
vec!["".to_owned()],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
@@ -507,11 +503,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.first_line(),
|
||||
Some("me")
|
||||
@@ -524,9 +516,9 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me mo".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"me mo".to_owned(),
|
||||
"8BITMIME".to_owned(),
|
||||
"SIZE 42".to_owned(),
|
||||
],
|
||||
)
|
||||
.first_line(),
|
||||
@@ -551,7 +543,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
vec![" ".to_owned()],
|
||||
)
|
||||
.first_line(),
|
||||
Some(" ")
|
||||
@@ -563,7 +555,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
vec![" ".to_owned()],
|
||||
)
|
||||
.first_line(),
|
||||
Some(" ")
|
||||
@@ -575,7 +567,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec!["".to_string()],
|
||||
vec!["".to_owned()],
|
||||
)
|
||||
.first_line(),
|
||||
Some("")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#[cfg(feature = "pool")]
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
use super::pool::sync_impl::Pool;
|
||||
@@ -38,6 +38,14 @@ impl Transport for SmtpTransport {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SmtpTransport {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut builder = f.debug_struct("SmtpTransport");
|
||||
builder.field("inner", &self.inner);
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpTransport {
|
||||
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
||||
///
|
||||
@@ -58,7 +66,7 @@ impl SmtpTransport {
|
||||
.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
|
||||
/// that don't take SMTPS connections.
|
||||
@@ -95,29 +103,106 @@ impl SmtpTransport {
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
/// * A 60-seconds timeout for smtp commands
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`SmtpTransport::relay`](#method.relay) or
|
||||
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
|
||||
let new = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
SmtpTransportBuilder::new(server)
|
||||
}
|
||||
|
||||
SmtpTransportBuilder {
|
||||
info: new,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
/// Creates a `SmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// The protocol, credentials, host and port can be provided in a single URL.
|
||||
/// 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
|
||||
///
|
||||
/// `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> {
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
@@ -141,6 +226,20 @@ pub struct SmtpTransportBuilder {
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
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
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
@@ -194,7 +293,7 @@ impl SmtpTransportBuilder {
|
||||
|
||||
/// 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`]
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
let client = SmtpClient { info: self.info };
|
||||
@@ -218,9 +317,9 @@ impl SmtpClient {
|
||||
/// Handles encryption and authentication
|
||||
pub fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match self.info.tls {
|
||||
let tls_parameters = match &self.info.tls {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -234,14 +333,14 @@ impl SmtpClient {
|
||||
)?;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
match self.info.tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
match &self.info.tls {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
conn = conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
Tls::Required(tls_parameters) => {
|
||||
conn = conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
@@ -252,3 +351,78 @@ impl SmtpClient {
|
||||
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(
|
||||
"smtps://user%40example.com:pa$$word%3F%22!@smtp.example.com:465",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 465);
|
||||
assert_eq!(
|
||||
builder.info.credentials,
|
||||
Some(Credentials::new(
|
||||
"user@example.com".to_owned(),
|
||||
"pa$$word?\"!".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(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ mod tests {
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
assert_eq!(format!("{}", XText(input)), (*expect).to_string());
|
||||
assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user