Compare commits

...

214 Commits

Author SHA1 Message Date
Paolo Barbolini
efee4b5d72 Fix warnings 2024-03-19 16:21:10 +01:00
Paolo Barbolini
57d7bf25cc Replace try_smtp macro with more resilient method 2024-03-19 16:21:10 +01:00
Paolo Barbolini
c52c596458 Drop ref syntax 2024-03-10 15:28:54 +01:00
Paolo Barbolini
53dee3e31f Drop need for NetworkStream::None variant 2024-03-10 15:28:54 +01:00
dependabot[bot]
c64cb0ff2e Bump mio from 0.8.10 to 0.8.11 (#946)
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 22:38:54 +01:00
Paolo Barbolini
10d7b197ed chore(cargo): bump base64 to v0.22 (#945) 2024-03-02 11:34:57 +01:00
Viktor Szépe
fb54855d5f Fix typos (#944) 2024-02-21 14:49:42 +01:00
ciffelia
157c4fb5ae docs(transport): fix error in "Available transports" table (#943) 2024-02-18 17:19:27 +01:00
Hodu Mayo
1196e332ee feat(transport-smtp): Support to SASL draft login challenge (#911) 2024-02-14 21:18:56 +01:00
Alexis Mousset
75770f7bc6 Add conversion from SMTP code to integer (#941) 2024-02-14 21:01:21 +01:00
Alexis Mousset
76d0929c94 Add a Cargo.lock (#942) 2024-02-14 20:50:54 +01:00
Paolo Barbolini
c3d00051b2 Prepare 0.11.4 (#936) 2024-01-28 08:08:26 +01:00
Birk Tjelmeland
12580d82f4 style(email): Change Part::body_raw to Part::format_body 2024-01-28 07:54:16 +01:00
Birk Tjelmeland
f7849078b8 fix(email): Fix mimebody DKIM body-hash computation 2024-01-28 07:54:16 +01:00
Paolo Barbolini
f2c94cdf4d chore(cargo): bump maud to v0.26 (#935) 2024-01-25 20:08:32 +01:00
Paolo Barbolini
74f64b81ab test(transport/smtp): test credentials percent decoding from URL (#934) 2024-01-25 20:05:31 +01:00
42triangles
39c71dbfd2 transport/smtp: percent decode credentials in URL (#932) 2024-01-12 11:43:30 +00:00
Paolo Barbolini
c1bf5dfda1 Prepare 0.11.3 (#929) 2024-01-02 18:45:34 +01:00
Paolo Barbolini
1c1fef8055 Drop once_cell dependency in favor of OnceLock from std (#928) 2024-01-02 11:53:47 +01:00
Paolo Barbolini
1540f16015 Upgrade rustls to v0.22 (#921) 2024-01-02 11:41:16 +01:00
Tobias Bieniek
330daa1173 transport/smtp: Implement Debug trait (#925) 2023-12-17 09:20:51 +01:00
Tobias Bieniek
47f2fe0750 transport/file: Derive Clone impls (#924) 2023-12-16 21:30:57 +01:00
Paolo Barbolini
8b6cee30ee Prepare 0.11.2 (#919) 2023-11-23 09:49:21 +01:00
Paolo Barbolini
62c16e90ef Bump idna to v0.5 (#918) 2023-11-23 08:23:25 +00:00
Paolo Barbolini
e0494a5f9d Bump boringssl crates to v4 (#915) 2023-11-19 11:49:43 +01:00
Paolo Barbolini
8c3bffa728 Bump MSRV to 1.70 (#916) 2023-11-19 11:42:49 +01:00
Paolo Barbolini
47eda90433 Prepare 0.11.1 (#910) 2023-10-24 23:47:49 +02:00
Paolo Barbolini
46ea8c48ac Ignore rustls deprecation warning 2023-10-24 22:55:16 +02:00
Paolo Barbolini
5f7063fdc3 Fix accidental disabling of webpki-roots setup (#909) 2023-10-24 22:54:22 +02:00
Paolo Barbolini
61c1f6bc6f Fix date in changelog 2023-10-15 17:22:48 +02:00
Paolo Barbolini
283e21f8d6 Prepare 0.11.0 (#899) 2023-10-15 16:21:32 +02:00
Paolo Barbolini
20c3701eb0 Fix doctests (#906) 2023-10-15 16:04:12 +02:00
Paolo Barbolini
74117d5cc6 ci: fix MSRV job (#905) 2023-09-23 09:16:42 +02:00
Marlon
bb49e0a46b Construct a SmtpTransport from a connection URL (#901) 2023-09-23 08:48:36 +02:00
Paolo Barbolini
42365478c2 Revert "Added Address:new_unchecked (#887)" (#904)
This reverts commit 7e6ffe8aea.
2023-09-01 16:06:48 +02:00
Hugo
94769242d1 docs: improve documentation for AsyncSmtpConnection (#903)
Closes: #902

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

This should help improve #556 a tiny bit.

Co-authored-by: finga <finga@onders.org>
2023-01-24 09:14:31 +01:00
Christian Mandery
3c2f996856 Fix broken URL for IETF user-agent draft (#849) 2023-01-16 11:34:30 +00:00
lfuerderer
9cae29dd07 Add Content-Type header in documentation example (#841)
* Add Content-Type header in documentation example

In the example showing how to build a message from a pure string, set
the Content-Type to text/plain explicitly.
This header constant also includes the phrase charset=utf-8 so that
non-ascii characters will be displayed correctly.

* cargo fmt

* Fix generated email example

Co-authored-by: Paolo Barbolini <paolo.barbolini@m4ss.net>
2022-12-16 22:04:29 +00:00
Paolo Barbolini
e1a146c8f8 cargo: bump base64 to 0.20 (#840) 2022-12-10 22:29:01 +00:00
Paolo Barbolini
840a19784a cargo: require quoted_printable ^0.4.6 (#837) 2022-12-07 15:37:38 +00:00
Paolo Barbolini
5a61ba36b5 ci: bump nightly used for rustfmt (#817) 2022-11-12 19:51:24 +01:00
Paolo Barbolini
dbf0e53c31 Fix latest clippy warnings (#830) 2022-11-12 17:54:20 +00:00
Paolo Barbolini
c914a07379 Update dependencies (#829) 2022-11-12 17:45:19 +00:00
Paolo Barbolini
2c4fa39523 Use cargo weak dependency features to reduce tokio features (#785) 2022-11-12 17:34:55 +00:00
Clément DOUIN
28f0af16be Mailbox displays wrongly when containing a comma and a non-ascii char in its name (#827) 2022-11-12 17:23:06 +00:00
Paolo Barbolini
f0614be555 Bump MSRV to 1.60 (#828) 2022-11-12 17:13:14 +00:00
André Cruz
a3fcdf263d fix(transport): return whole smtp error string (#821)
We were only returning the first line of the error message,
but that may not be enough to get the whole context of the error.
Since we have already parsed the response, just return the whole error
to the user.

Related to #694
2022-09-22 16:30:04 +02:00
Paolo Barbolini
d4da2e1f14 ci: switch to Swatinem/rust-cache@v2 (#819) 2022-09-10 13:19:49 +02:00
Paolo Barbolini
5655958288 clippy: fix latest warning (#818) 2022-09-10 10:59:49 +00:00
Paolo Barbolini
11b4acf0cd Improve header encoding and wrapping (#811)
Also cleans up the encoder a lot, removing some
complicated logic introduced by the initial round
of implementation
2022-09-10 12:40:00 +02:00
Paolo Barbolini
b3b5df285a Bump idna to 0.3 (#816) 2022-09-09 07:38:59 +00:00
Tom Dryer
3c051d52e7 Remove dependency on regex crate (#815)
Replace implementation of DKIM body canonicalization to remove
dependency on the `regex` crate.

Fixes #768.
2022-08-22 09:44:10 +02:00
André Cruz
d6128a146e use a generic transport trait for async connections (#805)
Rely on a generic transport trait and allow passing in one. This
will enable use cases where we don't have a real Tokio TCP stream,
or have to bind a specific source address before establishing
the connection.
2022-07-27 09:40:13 +02:00
André Cruz
fab6680150 Fix clippy warnings (#807)
Depending on the features chosen these attributes were left
unused.
2022-07-25 18:00:17 +02:00
Paolo Barbolini
0c9fc6cb71 Prepare 0.10.1 (#804) 2022-07-20 10:44:55 +02:00
Paolo Barbolini
2228cbdf93 Fix SMTP dot stuffing (#803) 2022-07-19 23:20:44 +02:00
André Cruz
17c95b0fa8 Ensure connection is closed on abort (#801)
When aborting a connection, ensure the underlying stream is closed
before we return.
2022-07-19 21:13:51 +02:00
Paolo Barbolini
62725af00a Improve TlsVersion docs and remember to re-export it (#800) 2022-07-18 07:40:46 +00:00
André Cruz
758bf1a4a7 Configurable minimum TLS version (#799)
Added support for configuring the minimum accepted TLS version. The
supported versions differ between TLS toolkits.
2022-07-17 13:11:14 +02:00
Paolo Barbolini
054c79f914 Document the boring-tls features in lib.rs (#798) 2022-07-16 09:45:44 +00:00
André Cruz
985fa7edc4 Add support for boring TLS (#797)
In contexts where FIPS certification is mandatory, having the
ability to use the certified boring TLS library is sometimes necessary.
Added initial support for it, only one TLS toolkit can be enabled at
one time.
2022-07-16 11:28:14 +02:00
Paolo Barbolini
9004d4ccc5 Add documentation to undocumented items (#793) 2022-06-29 22:44:29 +02:00
Paolo Barbolini
10171f8c75 Prepare 0.10.0 release (#538) 2022-06-29 21:17:37 +02:00
Paolo Barbolini
99e805952d Make it possible to keep the Bcc header when building a message (#792) 2022-06-29 21:08:27 +02:00
Paolo Barbolini
2d21dde5a1 Add autoconfigure.rs example (#787) 2022-06-17 06:35:07 +00:00
Paolo Barbolini
6fec936c0c Remove useless vec! allocations (#786) 2022-06-16 18:17:19 +00:00
Paolo Barbolini
22dfa5aa96 MessageBuilder: improve order headers are defined in (#783) 2022-06-16 17:53:54 +00:00
Paolo Barbolini
44e4cfd622 clippy: make rules stricter (#784) 2022-06-16 19:42:13 +02:00
Paolo Barbolini
7ea3d38a00 Mailboxes: add FromIterator impl and optimize Extend impl (#782) 2022-06-11 18:18:55 +00:00
Paolo Barbolini
73b89f5a9f clippy: fix latest warnings (#781) 2022-06-11 16:31:12 +00:00
Paolo Barbolini
1ec1b705c9 Prepare 0.10.0-rc.7 (#777) 2022-06-04 11:47:50 +02:00
Paolo Barbolini
e4006518fe Stop using the regex crate for parsing addresses (#776) 2022-06-03 13:39:57 +00:00
Paolo Barbolini
b33dd562fc Fix and improve header wrapping (#774)
Instead of injecting spaces to ensure that lines stay under 76 characters only wrap at whitespace characters. This avoids changing the headers.

A best-effort to keep lines under 76 characters is still done, however it is only done at whitespace. Notably there is no hard wrap enforced. This means that it is possible for headers to break the 1000 character line-length limit in the specification. It is just hoped that the receiver will allow long lines in this case.

Closes #688

Co-authored-by: Kevin Cox <kevincox@kevincox.ca>
2022-06-03 15:24:53 +02:00
Paolo Barbolini
65958df14f Use pretty_assertions for all message tests (#775) 2022-06-02 12:20:06 +00:00
James Hillyerd
50628af5fd README.md: Use IPv4 notation for localhost (#771) 2022-05-30 17:20:51 +00:00
Paolo Barbolini
cf858cc682 Move most email body encoding to email-encoding (#769) 2022-05-30 15:12:42 +00:00
Paolo Barbolini
f9a4b5ba89 Work around async-global-executor bumping MSRV too early (#773) 2022-05-30 15:01:40 +00:00
Jacob Halsey
1391a834ce #715: Support setting the local IP address to connect from (#762)
This adds a `local_address: Option<IpAddr>` parameter to the synchronous, and tokio connect functions.

(As far as I can see there is no current way to support this in async-std, because the library doesn't provide any way to do an async connect for an existing socket)
2022-05-29 07:05:39 +00:00
André Cruz
e6b4529896 use email_address crate for checking formats (#763)
The email_address crate is more strict in validating user and domain
parts of email addresses. For example, it verifies the total size
of the local part, which the current method does not, and this has
caused upstream servers to fail to accept an email.
2022-05-26 19:21:14 +02:00
Kevin Cox
ca5cb3f8f7 Fix encoded header signing. (#765)
The header needs to be properly formatted so that Unicode characters are encoded the same way they will be in the final message. Previously the logical header value was being encoded.

A notable example is that a `'` in the `To:` header needs to be encoded. This was being encoded incorrectly.
2022-05-26 07:44:14 +02:00
Kevin Cox
1e2279457e Add editorconfig file. (#766)
Makes it easy for everyone to use the preferred settings.

https://editorconfig.org/
2022-05-18 13:07:38 +00:00
Kevin Cox
961364cc29 Remove unnecessary clone. (#767)
This is backwards-incompatible but hopefully is an acceptable change for a pre-release. The upgrade path is straight forward.
2022-05-18 10:51:02 +02:00
Paolo Barbolini
b0db759e5f Prepare 0.10.0-rc.6 (#761) 2022-04-29 15:59:36 +02:00
Paolo Barbolini
5daf5d397a Fix parsing Mailboxes with a comma in the name (#760) 2022-04-26 12:18:12 +02:00
Paolo Barbolini
3f1647fa48 Bump dependencies (#759) 2022-04-25 09:17:58 +00:00
Paolo Barbolini
fd106d9b0c Bump rsa crate to the final 0.6.0 release (#758) 2022-04-14 09:39:30 +00:00
Vincent Breitmoser
c1d37d54b4 Use +0000 timezone format in Date header (#756)
Since the Date we emit is UTC, it's correct to use "+0000". The
previously used -0000 timezone indicator means "no timezone info".
2022-04-10 08:34:50 +02:00
David Krasnitsky
efa0d58778 Improve compiler error messages (#754) 2022-04-07 05:03:28 +00:00
Paolo Barbolini
9567b23f4d Prepare 0.10.0-rc.5 (#750) 2022-04-02 10:21:38 +02:00
Paolo Barbolini
f77376fa19 Update to released email-encoding crate (#749) 2022-04-02 08:10:25 +00:00
Paolo Barbolini
6e35b9b30d Bump RustCrypto crates (#748) 2022-04-02 07:55:36 +00:00
Sven-Hendrik Haase
c24213c850 Add message logging to StubTransport (#744)
This makes it more useful as a testing tool as it now allows you to retrieve
all messages sent via this transport.
2022-03-25 08:22:47 +01:00
fluentpwn
8b40e438fd Year update (#725) 2022-03-24 06:06:24 +00:00
Paolo Barbolini
e1462b2d1b Bump MSRV to 1.56 - Edition 2021 (#745) 2022-03-24 05:52:28 +00:00
Paolo Barbolini
96b42515cd Don't run headers that don't need encoding though the encoder (#739) 2022-02-17 19:18:54 +00:00
Paolo Barbolini
1ea4987023 Encode mailbox headers through email-encoding (#737) 2022-02-17 20:00:43 +01:00
Paolo Barbolini
9273d24e54 Use nightly rustfmt features to improve code style (#734)
* format_code_in_doc_comments

* imports_granularity

* group_imports

* Add ci job
2022-02-12 20:03:37 +01:00
Paolo Barbolini
7a0dd5bd92 clippy: deny string_add (#735) 2022-02-12 17:12:41 +00:00
Paolo Barbolini
9a8aa46dba Start future proofing the DKIM API (#733) 2022-02-12 16:41:34 +00:00
Paolo Barbolini
0377ea29b7 Dkim improvements (#732)
Some tweaks to the DKIM implementation to make it a tiny bit more readable in some places and allocate less in general.
2022-02-12 16:19:06 +00:00
Paolo Barbolini
89e5b9083e Bump dev dependencies (#730) 2022-02-12 10:25:42 +00:00
Paolo Barbolini
8c370e28c9 Follow RFC 2231 in order to properly encode the Content-Disposition header (#685)
Uses RFC 2231 to encode the Content-Disposition header
2022-02-12 10:17:44 +00:00
Paolo Barbolini
3eed80ef30 Bump MSRV to 1.53 (#731) 2022-02-12 10:02:44 +00:00
Paolo Barbolini
dbb135c533 Introduce HeaderValue (#729) 2022-02-12 10:24:52 +01:00
Gaëtan Duchaussois
4c5f02b4f6 feat(email): add dkim signing capacity to message (#670) 2022-02-12 09:21:35 +01:00
Paolo Barbolini
f02542841c Bump rustls-pemfile to 0.3 (#728) 2022-02-05 18:34:13 +01:00
Jacob Halsey
29c34adc25 feature(transport-smtp): make peer certificate available in SmtpConnection (#716) 2022-01-19 11:28:34 +01:00
Kevin Cox
5e3ebbb189 Properly quote mailbox name (#700)
This quoting is performed according to https://datatracker.ietf.org/doc/html/rfc2822.

Note that the obsolete phrase specification allows periods in the mailbox name. This does not implement the obsolete specification, instead periods force the mailbox to use a quoted string.

Fixes #698
2021-11-17 18:28:34 +01:00
Dirkjan Ochtman
60399a93cc Forward first line of response for negative responses (#701) 2021-11-15 17:41:54 +01:00
facklambda
a48bc8a1b2 docs(smtp-transport) Add Troubleshooting steps (#692) 2021-11-12 21:53:12 +01:00
Paolo Barbolini
94cc0149d1 Prepare 0.10.0-rc.4 (#691) 2021-10-29 09:22:53 +02:00
Filip Gospodinov
a89383cdb6 Re-enable pool tests (#684)
The pool tests have been implicitly disabled by testing
for a feature that has been removed.
2021-10-20 19:31:56 +02:00
Filip Gospodinov
592593f4b8 Expose test_connected via transport (#677)
It is useful for application developers to validate SMTP
settings by testing the connection.

Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-10-20 16:42:49 +02:00
Paolo Barbolini
97d3c760c0 Update rustls to 0.20 (#648) 2021-10-20 14:37:16 +02:00
Alexis Mousset
8f28b0c341 fix(transport): Use the sendmail command in PATH by default (#682)
This will allow the transport to work with default settings on more systems,
while preserving the ability to use a specific binary.
2021-10-16 09:49:45 +02:00
Paolo Barbolini
dc9c5df210 Sync pool impl (#644)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-10-16 09:39:06 +02:00
Gaëtan Duchaussois
c9b3fa0baa docs(all): Add instruction to launch fake smtp server (#681) 2021-10-11 16:41:03 +02:00
Paolo Barbolini
addf8754dd smtp: don't send QUIT to connections that failed the STARTTLS handshake (#679) 2021-10-06 17:25:50 +00:00
Jacob Mischka
af157c5f26 Add From<Mime> under mime03 flag (#676)
Closes #615
2021-10-05 11:22:25 +02:00
TornaxO7
3e8988ae55 ContentTransferEncoding derives changes (#652) 2021-09-29 06:42:54 +02:00
Paolo Barbolini
941a00bcaa Bump MSRV to 1.52.1 (#671) 2021-09-12 18:40:26 +00:00
Paolo Barbolini
14079bff8c Give a compiletime error when using an incorrect combination of TLS features (#666) 2021-09-07 20:43:07 +02:00
Paolo Barbolini
696c06e8d7 Bump nom to v7 (#663) 2021-08-22 14:54:09 +02:00
Paolo Barbolini
d4f7618898 Better document how Attachments can be used (#658) 2021-08-20 08:17:36 +00:00
Christopher Vittal
e0a0a2e624 feat(address): Add TryFrom<String> for Address (#660)
Refactor the validation part of from_str into its own function as the
behavior for try_from is identical.
2021-08-20 09:09:49 +02:00
Paolo Barbolini
9ab6bb56d3 Fix broken <summary> style (#659) 2021-08-20 04:48:30 +02:00
Paolo Barbolini
e1d3778329 Revert "Allow a Message to be decomposed into a MessageBuilder (#633)" (#649)
This reverts commit aadcc0f83c.

Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-08-02 12:20:02 +00:00
Paolo Barbolini
623d69c553 Fix #653 (#654) 2021-08-02 09:42:56 +00:00
Paolo Barbolini
55c2618201 Fix latest clippy warnings (#655) 2021-08-02 09:26:34 +00:00
Paolo Barbolini
9f550bce86 Bump MSRV to 1.49 (#656) 2021-08-02 09:26:25 +00:00
TornaxO7
e875d9ff64 ContentType Documentation (#642)
Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2021-07-06 11:46:42 +02:00
Alex Feldman-Crough
aadcc0f83c Allow a Message to be decomposed into a MessageBuilder (#633) 2021-06-30 18:21:55 +00:00
Paolo Barbolini
b534a18017 Async pool implementation (#637) 2021-06-29 15:23:47 +00:00
TornaxO7
0684bccd47 Implement Serialize and Deserialize for ContentType (#643) 2021-06-29 10:58:39 +02:00
Paolo Barbolini
4471759221 Implement connection timeouts for AsyncSmtpTransport (#635) 2021-06-17 22:39:48 +00:00
Paolo Barbolini
ed454819ee Refactor pool module (#636)
* Move pool to it's own module

* pool: deprecate configuring the connection timeout
2021-06-13 14:36:47 +02:00
Alexis Mousset
47cad567b0 Prepare 0.10.0-rc.3 (#629) 2021-05-22 19:52:38 +02:00
Alexis Mousset
b0e2fc9bca fix(transport-smtp): Fix transparency codec (#627)
It fails to add transparency when a period is preceded by two
successive CRLF.

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2021-05-22 19:41:29 +02:00
Alexis Mousset
1d8249165c Makes more things private and add missing docs (#621) 2021-05-19 18:51:03 +02:00
Alexis Mousset
98fc0cb2f3 Prepare 0.10.0-rc.2 (#624) 2021-05-18 18:12:10 +02:00
Alexis Mousset
0439bab874 fix(builder): Don't include Bcc headers in formatted messages (#623)
fixes #622
2021-05-18 18:03:20 +02:00
Alexis Mousset
504fc51b26 Prepare 0.10.0-rc.1 (#620) 2021-05-14 17:48:42 +02:00
Paolo Barbolini
d54343cf00 Remove Part from the public API (#619) 2021-05-14 17:27:03 +02:00
Alexis Mousset
904789ac3d feat(builder): Add helper methods for attachments and text (#618) 2021-05-14 16:59:08 +02:00
Paolo Barbolini
94cae6df0d Drop tokio 0.2 support (#617) 2021-05-12 19:09:30 +02:00
Alexis Mousset
f17dccc46d builder: Fix Message-ID header (#614) 2021-05-04 18:45:29 +02:00
Paolo Barbolini
7e7f05eb45 Prepare 0.10.0-beta.4 (#613) 2021-05-04 18:31:55 +02:00
Paolo Barbolini
99df9e8d7c Headers insert_raw -> append_raw, set_raw -> insert_raw (#612) 2021-05-04 18:19:21 +02:00
Paolo Barbolini
1b5109b6ac Add docs to Headers (#610) 2021-05-02 11:10:04 +02:00
Alexis Mousset
a4be3c4cd8 Add InvalidHeaderName error (#608)
* Add InvalidHeaderName error
2021-05-01 22:00:33 +02:00
Paolo Barbolini
4586f2ad8a Remove useless clones (#609) 2021-05-01 18:22:53 +02:00
Paolo Barbolini
31de9e508b Replace hyperx Header and Headers with our own implementation (#607)
* Replace hyperx Header and Headers with our own implementation

* Remove utf8_b

* Add RFC 1522 encoder

* Fix most tests

* Throw away old tests

* Header encoding tests

* Fix slicing in the middle of a char

* Content-Disposition after rebase

* Fix the rest of the tests

* Fix useless clone clippy warnings

* Remove Headers::get_raw_mut

* HeaderName::new_from_ascii fallible API

* Tidy up HeaderName::new_from_ascii_str

* HeaderName::new_from_ascii(_str) tests
2021-05-01 13:27:00 +02:00
Paolo Barbolini
69334fe5eb Replace the hyperx ContentDisposition header with our own implementation (#601) 2021-04-24 18:21:29 +02:00
Paolo Barbolini
2ad2444183 Replace the hyperx ContentLocation header with our own implementation (#603) 2021-04-24 18:00:36 +02:00
Paolo Barbolini
8afa442e93 Add missing doc(cfg(..)) attributes (#604) 2021-04-20 19:09:28 +02:00
Paolo Barbolini
486e0f9d50 Replace hyperx ContentType header with our own implementation (#598)
* Replace hyperx ContentType header with our own implementation

* Let's not forget ContentTypeErr

* Adress code review comment
2021-04-08 08:40:07 +00:00
Paolo Barbolini
acc4ff4898 Replace hyperx Date header with our own implementation (#597) 2021-04-08 07:55:20 +02:00
Paolo Barbolini
1728d57c34 Stop using the uuid crate for generating the Message-Id (#602) 2021-04-07 18:38:56 +00:00
Alex Wennerberg
53bfb65423 Replace rand with fastrand (#600)
We don't need cryptographically secure random numbers, this simplifies
the dependency tree and speeds up builds.
2021-04-06 21:32:43 +02:00
Paolo Barbolini
61b08814c9 Avoid useless allocations while formatting headers (#599) 2021-04-06 17:02:37 +00:00
Paolo Barbolini
0e74042b4e Convert String Body line-endings to CRLF (#588) 2021-04-01 12:29:51 +02:00
Paolo Barbolini
29affe9398 Seal header contents (#591) 2021-04-01 12:18:38 +02:00
Jupp56
b10f6ff8de Fix: example does not compile (#592)
A missing simple string conversion prevented the example code from compiling.
2021-03-31 12:14:49 +02:00
Paolo Barbolini
2002a9d75a tls: use rustls if both native-tls and rustls-tls are enabled (#586)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-03-30 09:06:26 +00:00
Paolo Barbolini
1193e1134d Bump MSRV to 1.46.0 (#587) 2021-03-30 08:40:05 +00:00
Alexis Mousset
7c6ade7afe Add a get started doc for transports (#577)
* Add a get started doc for transports

This will help users not familiar with email infrastructure.
2021-03-19 08:34:13 +00:00
Paolo Barbolini
3bc729ca64 Remove MockStream and all internal uses of it (#580)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-03-19 08:20:05 +00:00
Paolo Barbolini
f041c00df7 Fix a few clippy warnings (#579) 2021-03-19 08:03:41 +00:00
Alexis Mousset
fe8dc4967d Prepare 0.10.0-beta.3 (#578) 2021-03-18 20:28:02 +00:00
Paolo Barbolini
137566a4e4 Improve docs in lib.rs (#574)
* Improve docs in lib.rs

* Typos
2021-03-18 08:02:21 +00:00
Paolo Barbolini
216c612931 Fix missing re-export of AsyncSmtpTransport when using async-std1 feature (#573) 2021-03-17 06:52:57 +00:00
Paolo Barbolini
a429a24913 Add missing Debug implementations (#570) 2021-03-14 10:17:07 +01:00
Paolo Barbolini
648bf2b2f6 chore: remove some uses of * (#569) 2021-03-14 09:22:59 +01:00
Alexis Mousset
509a623a27 feat(transport): Seal file and sendmail error types (#567) 2021-03-14 07:42:52 +00:00
Paolo Barbolini
a681c6b49d Remove From implementations on Error for file and sendmail transport (#566) 2021-03-13 17:41:44 +00:00
Alexis Mousset
22efe341fe feat(builder): Seal SMTP error type (#564)
* feat(builder): Seal SMTP error type

* More precise error types
2021-03-13 17:15:21 +00:00
Paolo Barbolini
97fba6a47e docs: improve docs for lettre::transport (#565) 2021-03-13 16:03:05 +00:00
Paolo Barbolini
f7066ac858 Fix various parts of the docs (#563) 2021-03-12 20:02:31 +01:00
Alexis Mousset
9379f2e328 Prepare 0.10.0-beta.2 2021-03-10 21:56:00 +00:00
Paolo Barbolini
05133a7102 Test using all supported async executors 2021-03-08 13:23:27 +01:00
Paolo Barbolini
d7d05bf48a Update deprecated imports 2021-03-08 10:59:28 +01:00
Paolo Barbolini
34ac265d60 Remove deprecated executor methods 2021-03-08 10:59:28 +01:00
Hari Konomi
bbf56de83d feat(transport-smtp): Call conn.quit() when pooled conns are released (#559)
* Implement CustomizeConnection::on_release() for SmtpConnection
2021-03-05 19:02:01 +01:00
87 changed files with 10726 additions and 3148 deletions

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
root = true
[*]
insert_final_newline = true
[*.rs]
indent_size = 4
indent_style = space

View File

@@ -7,11 +7,13 @@ on:
- master - master
env: env:
RUSTFLAGS: "--cfg lettre_ignore_tls_mismatch"
RUSTDOCFLAGS: "--cfg lettre_ignore_tls_mismatch"
RUST_BACKTRACE: full RUST_BACKTRACE: full
jobs: jobs:
rustfmt: rustfmt:
name: rustfmt / stable name: rustfmt / nightly-2023-06-22
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -20,7 +22,7 @@ jobs:
- name: Install rust - name: Install rust
run: | run: |
rustup update --no-self-update stable rustup default nightly-2023-06-22
rustup component add rustfmt rustup component add rustfmt
- name: cargo fmt - name: cargo fmt
@@ -50,18 +52,12 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-check
- name: Install rust - name: Install rust
run: rustup update --no-self-update stable run: rustup update --no-self-update stable
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install cargo hack - name: Install cargo hack
run: cargo install cargo-hack --debug run: cargo install cargo-hack --debug
@@ -79,27 +75,21 @@ jobs:
rust: stable rust: stable
- name: beta - name: beta
rust: beta rust: beta
- name: 1.45.2 - name: '1.70'
rust: 1.45.2 rust: '1.70'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}
- name: Install rust - name: Install rust
run: | run: |
rustup default ${{ matrix.rust }} rustup default ${{ matrix.rust }}
rustup update --no-self-update ${{ matrix.rust }} rustup update --no-self-update ${{ matrix.rust }}
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install postfix - name: Install postfix
run: | run: |
DEBIAN_FRONTEND=noninteractive sudo apt-get update DEBIAN_FRONTEND=noninteractive sudo apt-get update
@@ -108,14 +98,37 @@ jobs:
- name: Run SMTP server - name: Run SMTP server
run: smtp-sink 2525 1000& run: smtp-sink 2525 1000&
- name: Install coredns
run: |
wget -q https://github.com/coredns/coredns/releases/download/v1.8.6/coredns_1.8.6_linux_amd64.tgz
tar xzf coredns_1.8.6_linux_amd64.tgz
- name: Start coredns
run: |
sudo ./coredns -conf testdata/coredns.conf &
sudo systemctl stop systemd-resolved
echo "nameserver 127.0.0.54" | sudo tee /etc/resolv.conf
- name: Install dkimverify
run: sudo apt -y install python3-dkim
- name: Work around early dependencies MSRV bump
run: |
cargo update -p anstyle --precise 1.0.2
cargo update -p clap --precise 4.3.24
cargo update -p clap_lex --precise 0.5.0
- name: Test with no default features - name: Test with no default features
run: cargo test --no-default-features run: cargo test --no-default-features
- name: Test with default features - name: Test with default features
run: cargo test run: cargo test
- name: Test with all features - name: Test with all features (-native-tls)
run: cargo test --all-features 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-std1,async-std1-rustls-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls-tls,tracing
# coverage: # coverage:
# name: Coverage # name: Coverage

1
.gitignore vendored
View File

@@ -4,4 +4,3 @@
lettre.sublime-* lettre.sublime-*
lettre.iml lettre.iml
target/ target/
/Cargo.lock

View File

@@ -1,11 +1,201 @@
<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)
#### Upgrade notes
* MSRV is now 1.60 ([#828])
#### Features
* Allow providing a custom `tokio` stream for `AsyncSmtpTransport` ([#805])
* Return whole SMTP error message ([#821])
#### Bug fixes
* Mailbox displays wrongly when containing a comma and a non-ascii char in its name ([#827])
* Require `quoted_printable` ^0.4.6 in order to fix encoding of tabs and spaces at the end of line ([#837])
#### Misc
* Increase tracing ([#848])
* Bump `idna` to 0.3 ([#816])
* Update `base64` to 0.21 ([#840] and [#851])
* Update `rsa` to 0.8 ([#829] and [#852])
[#805]: https://github.com/lettre/lettre/pull/805
[#816]: https://github.com/lettre/lettre/pull/816
[#821]: https://github.com/lettre/lettre/pull/821
[#827]: https://github.com/lettre/lettre/pull/827
[#828]: https://github.com/lettre/lettre/pull/828
[#829]: https://github.com/lettre/lettre/pull/829
[#837]: https://github.com/lettre/lettre/pull/837
[#840]: https://github.com/lettre/lettre/pull/840
[#848]: https://github.com/lettre/lettre/pull/848
[#851]: https://github.com/lettre/lettre/pull/851
[#852]: https://github.com/lettre/lettre/pull/852
<a name="v0.10.1"></a>
### v0.10.1 (2022-07-20)
#### Features
* Add `boring-tls` support for `SmtpTransport` and `AsyncSmtpTransport`. The latter is only supported with the tokio runtime. ([#797]) ([#798])
* Make the minimum TLS version configurable. ([#799]) ([#800])
#### Bug Fixes
* Ensure connections are closed on abort. ([#801])
* Fix SMTP dot stuffing. ([#803])
[#797]: https://github.com/lettre/lettre/pull/797
[#798]: https://github.com/lettre/lettre/pull/798
[#799]: https://github.com/lettre/lettre/pull/799
[#800]: https://github.com/lettre/lettre/pull/800
[#801]: https://github.com/lettre/lettre/pull/801
[#803]: https://github.com/lettre/lettre/pull/803
<a name="v0.10.0"></a> <a name="v0.10.0"></a>
### v0.10.0 (unreleased) ### v0.10.0 (2022-06-29)
#### Upgrade notes #### Upgrade notes
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward: Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
* MSRV is now 1.45.2 * MSRV is now 1.56.0
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message` * The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message`
and make sure to enable the `builder` feature (it's enabled by default). and make sure to enable the `builder` feature (it's enabled by default).
* `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate, * `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate,
@@ -14,7 +204,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
#### Features #### Features
* Add `tokio` 0.2 and 1.0 support * Add `tokio` 1 support
* Add `rustls` support * Add `rustls` support
* Add `async-std` support. NOTE: native-tls isn't supported when using async-std for the smtp transport. * Add `async-std` support. NOTE: native-tls isn't supported when using async-std for the smtp transport.
* Allow enabling multiple SMTP authentication mechanisms * Allow enabling multiple SMTP authentication mechanisms
@@ -29,9 +219,11 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
* Refactor `TlsParameters` implementation to not expose the internal TLS library * Refactor `TlsParameters` implementation to not expose the internal TLS library
* `FileTransport` writes emails into `.eml` instead of `.json` * `FileTransport` writes emails into `.eml` instead of `.json`
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility) * When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
* The `sendmail` and `file` transports aren't enabled by default anymore.
* The `new` method of `ClientId` is deprecated * The `new` method of `ClientId` is deprecated
* Rename `serde-impls` feature to `serde` * Rename `serde-impls` feature to `serde`
* The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of
`/usr/bin/sendmail`.
#### Bug Fixes #### Bug Fixes
@@ -52,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 `hostname` to 0.3
* Update to `nom` 6 * Update to `nom` 6
* Replace `log` with `tracing` * Replace `log` with `tracing`
* Move CI to Github Actions * Move CI to GitHub Actions
* Use criterion for benchmarks * Use criterion for benchmarks
<a name="v0.9.2"></a> <a name="v0.9.2"></a>

View File

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

2660
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge) # remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.0-beta.1" version = "0.11.4"
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
homepage = "https://lettre.rs" homepage = "https://lettre.rs"
@@ -10,7 +10,8 @@ license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"] authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
categories = ["email", "network-programming"] categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"] keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2018" edition = "2021"
rust-version = "1.70"
[badges] [badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" } is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -18,33 +19,37 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
idna = "0.2" chumsky = "0.9"
idna = "0.5"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder # builder
hyperx = { version = "1", optional = true, features = ["headers"] } httpdate = { version = "1", optional = true }
mime = { version = "0.3.4", optional = true } mime = { version = "0.3.4", optional = true }
uuid = { version = "0.8", features = ["v4"] } fastrand = { version = "2.0", optional = true }
rand = { version = "0.8", optional = true } quoted_printable = { version = "0.5", optional = true }
quoted_printable = { version = "0.4", optional = true } base64 = { version = "0.22", optional = true }
base64 = { version = "0.13", optional = true } email-encoding = { version = "0.2", optional = true }
once_cell = "1"
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
# file transport # file transport
uuid = { version = "1", features = ["v4"], optional = true }
serde = { version = "1", optional = true, features = ["derive"] } serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
# smtp # smtp-transport
nom = { version = "6", default-features = false, features = ["alloc"], optional = true } nom = { version = "7", optional = true }
r2d2 = { version = "0.8", optional = true } # feature
hostname = { version = "0.3", optional = true } # feature hostname = { version = "0.3", optional = true } # feature
socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true }
## tls ## tls
native-tls = { version = "0.2", optional = true } # feature native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true } rustls = { version = "0.22.1", optional = true }
webpki = { version = "0.21", optional = true } rustls-pemfile = { version = "2", optional = true }
webpki-roots = { version = "0.21", optional = true } rustls-native-certs = { version = "0.7", optional = true }
webpki-roots = { version = "0.26", optional = true }
boring = { version = "4", optional = true }
# async # async
futures-io = { version = "0.3.7", optional = true } futures-io = { version = "0.3.7", optional = true }
@@ -52,59 +57,79 @@ futures-util = { version = "0.3.7", default-features = false, features = ["io"],
async-trait = { version = "0.1", optional = true } async-trait = { version = "0.1", optional = true }
## async-std ## async-std
async-std = { version = "1.8", optional = true, features = ["unstable"] } async-std = { version = "1.8", optional = true }
#async-native-tls = { version = "0.3.3", optional = true } #async-native-tls = { version = "0.3.3", optional = true }
async-rustls = { version = "0.2", optional = true } futures-rustls = { version = "0.25", optional = true }
## tokio ## tokio
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["fs", "process", "tcp", "dns", "io-util"], optional = true } tokio1_crate = { package = "tokio", version = "1", optional = true }
tokio02_native_tls_crate = { package = "tokio-native-tls", version = "0.1", optional = true }
tokio02_rustls = { package = "tokio-rustls", version = "0.15", optional = true }
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "process", "net", "io-util"], optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true } tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.22", 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.9", optional = true }
ed25519-dalek = { version = "2", optional = true }
# email formats
email_address = { version = "0.2.1", default-features = false }
[dev-dependencies] [dev-dependencies]
criterion = "0.3" pretty_assertions = "1"
tracing-subscriber = "0.2.10" criterion = "0.5"
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
tracing-subscriber = "0.3"
glob = "0.3" glob = "0.3"
walkdir = "2" walkdir = "2"
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] } tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] } async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1" serde_json = "1"
maud = "0.22.1" maud = "0.26"
[[bench]] [[bench]]
harness = false harness = false
name = "transport_smtp" name = "transport_smtp"
[[bench]]
harness = false
name = "mailbox_parsing"
[features] [features]
default = ["smtp-transport", "native-tls", "hostname", "r2d2", "builder"] default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"] builder = ["dep:httpdate", "dep:mime", "dep:fastrand", "dep:quoted_printable", "dep:email-encoding"]
mime03 = ["dep:mime"]
# transports # transports
file-transport = [] file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "serde_json", "file-transport"] file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
sendmail-transport = [] sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
smtp-transport = ["base64", "nom"] smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
rustls-tls = ["webpki", "webpki-roots", "rustls"] pool = ["dep:futures-util"]
rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile"]
boring-tls = ["dep:boring"]
# async # async
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"] async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"] #async-std1-native-tls = ["async-std1", "native-tls", "dep:async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "async-rustls"] async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
tokio02 = ["tokio02_crate", "async-trait", "futures-io", "futures-util"] tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
tokio02-native-tls = ["tokio02", "native-tls", "tokio02_native_tls_crate"] tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
tokio02-rustls-tls = ["tokio02", "rustls-tls", "tokio02_rustls"] tokio1-rustls-tls = ["tokio1", "rustls-tls", "dep:tokio1_rustls"]
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"] tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"] dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
[[example]]
name = "autoconfigure"
required-features = ["smtp-transport", "native-tls"]
[[example]] [[example]]
name = "basic_html" name = "basic_html"
@@ -130,14 +155,6 @@ required-features = ["smtp-transport", "native-tls", "builder"]
name = "smtp_selfsigned" name = "smtp_selfsigned"
required-features = ["smtp-transport", "native-tls", "builder"] required-features = ["smtp-transport", "native-tls", "builder"]
[[example]]
name = "tokio02_smtp_tls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
[[example]]
name = "tokio02_smtp_starttls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
[[example]] [[example]]
name = "tokio1_smtp_tls" name = "tokio1_smtp_tls"
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"] required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]

View File

@@ -1,5 +1,5 @@
Copyright (c) 2014-2020 Alexis Mousset <contact@amousset.me> Copyright (c) 2014-2022 Alexis Mousset <contact@amousset.me>
Copyright (c) 2019-2020 Paolo Barbolini <paolo@paolo565.org> Copyright (c) 2019-2022 Paolo Barbolini <paolo@paolo565.org>
Copyright (c) 2018 K. <kayo@illumium.org> Copyright (c) 2018 K. <kayo@illumium.org>
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any

View File

@@ -28,22 +28,14 @@
</div> </div>
<div align="center"> <div align="center">
<a href="https://deps.rs/crate/lettre/0.10.0-beta.1"> <a href="https://deps.rs/crate/lettre/0.11.4">
<img src="https://deps.rs/crate/lettre/0.10.0-beta.1/status.svg" <img src="https://deps.rs/crate/lettre/0.11.4/status.svg"
alt="dependency status" /> alt="dependency status" />
</a> </a>
</div> </div>
--- ---
**NOTE**: this readme refers to the 0.10 version of lettre, which is
still being worked on. The master branch and the alpha releases will see
API breaking changes and some features may be missing or incomplete until
the stable 0.10.0 release is out.
Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x) branch for stable releases.
---
## Features ## Features
Lettre provides the following features: Lettre provides the following features:
@@ -58,18 +50,24 @@ Lettre does not provide (for now):
* Email parsing * Email parsing
## 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.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 ## Example
This library requires Rust 1.45 or newer. This library requires Rust 1.70 or newer.
To use this library, add the following to your `Cargo.toml`: To use this library, add the following to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
lettre = "0.10.0-beta.1" lettre = "0.11"
``` ```
```rust,no_run ```rust,no_run
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport}; use lettre::{Message, SmtpTransport, Transport};
@@ -78,10 +76,11 @@ let email = Message::builder()
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.body("Be happy!") .header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com") let mailer = SmtpTransport::relay("smtp.gmail.com")
@@ -92,16 +91,36 @@ let mailer = SmtpTransport::relay("smtp.gmail.com")
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
``` ```
## Not sure of which connect options to use?
Clone the lettre git repository and run the following command (replacing `SMTP_HOST` with your SMTP server's hostname)
```shell
cargo run --example autoconfigure SMTP_HOST
```
## Testing ## Testing
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
Alternatively only unit tests can be run by doing `cargo test --lib`. Alternatively only unit tests can be run by doing `cargo test --lib`.
## Troubleshooting
These are general steps to be followed when troubleshooting SMTP related issues.
- Ensure basic connectivity, ensure requisite ports are open and daemons are listening.
- Confirm that your service provider allows traffic on the ports being used for mail transfer.
- Check SMTP relay authentication and configuration.
- Validate your DNS records. (DMARC, SPF, DKIM, MX)
- Verify your SSL/TLS certificates are setup properly.
- Investigate if filtering, formatting, or filesize limits are causing messages to be lost, delayed, or blocked by relays or remote hosts.
## Code of conduct ## Code of conduct
Anyone who interacts with Lettre in any space, including but not limited to Anyone who interacts with Lettre in any space, including but not limited to

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ This folder contains examples showing how to use lettre in your own projects.
- [smtp_starttls.rs] - Send an email over SMTP with STARTTLS and authenticating with username and password. - [smtp_starttls.rs] - Send an email over SMTP with STARTTLS and authenticating with username and password.
- [smtp_selfsigned.rs] - Send an email over SMTP encrypted with TLS using a self-signed certificate and authenticating with username and password. - [smtp_selfsigned.rs] - Send an email over SMTP encrypted with TLS using a self-signed certificate and authenticating with username and password.
- The [smtp_tls.rs] and [smtp_starttls.rs] examples also feature `async`hronous implementations powered by [Tokio](https://tokio.rs/). - The [smtp_tls.rs] and [smtp_starttls.rs] examples also feature `async`hronous implementations powered by [Tokio](https://tokio.rs/).
These files are prefixed with `tokio02_`, `tokio1_` or `asyncstd1_`. These files are prefixed with `tokio1_` or `asyncstd1_`.
[basic_html.rs]: ./basic_html.rs [basic_html.rs]: ./basic_html.rs
[maud_html.rs]: ./maud_html.rs [maud_html.rs]: ./maud_html.rs

View File

@@ -1,6 +1,6 @@
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncStd1Transport, Message, AsyncStd1Executor, AsyncTransport, Message,
}; };
#[async_std::main] #[async_std::main]
@@ -12,10 +12,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS // Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<AsyncStd1Executor> = let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
@@ -27,6 +28,6 @@ async fn main() {
// Send the email // Send the email
match mailer.send(email).await { match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -1,6 +1,6 @@
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncStd1Transport, Message, AsyncStd1Executor, AsyncTransport, Message,
}; };
#[async_std::main] #[async_std::main]
@@ -12,10 +12,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer: AsyncSmtpTransport<AsyncStd1Executor> = let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
@@ -27,6 +28,6 @@ async fn main() {
// Send the email // Send the email
match mailer.send(email).await { match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

93
examples/autoconfigure.rs Normal file
View File

@@ -0,0 +1,93 @@
use std::{env, process, time::Duration};
use lettre::SmtpTransport;
fn main() {
tracing_subscriber::fmt::init();
let smtp_host = match env::args().nth(1) {
Some(smtp_host) => smtp_host,
None => {
println!("Please provide the SMTP host as the first argument to this command");
process::exit(1);
}
};
// TLS wrapped connection
{
tracing::info!(
"Trying to establish a TLS wrapped connection to {}",
smtp_host
);
let transport = SmtpTransport::relay(&smtp_host)
.expect("build SmtpTransport::relay")
.timeout(Some(Duration::from_secs(10)))
.build();
match transport.test_connection() {
Ok(true) => {
tracing::info!("Successfully connected to {} via a TLS wrapped connection (SmtpTransport::relay). This is the fastest option available for connecting to an SMTP server", smtp_host);
}
Ok(false) => {
tracing::error!("Couldn't connect to {} via a TLS wrapped connection. No more information is available", smtp_host);
}
Err(err) => {
tracing::error!(err = %err, "Couldn't connect to {} via a TLS wrapped connection", smtp_host);
}
}
}
println!();
// Plaintext connection which MUST then successfully upgrade to TLS via STARTTLS
{
tracing::info!("Trying to establish a plaintext connection to {} and then updating it via the SMTP STARTTLS extension", smtp_host);
let transport = SmtpTransport::starttls_relay(&smtp_host)
.expect("build SmtpTransport::starttls_relay")
.timeout(Some(Duration::from_secs(10)))
.build();
match transport.test_connection() {
Ok(true) => {
tracing::info!("Successfully connected to {} via a plaintext connection which then got upgraded to TLS via the SMTP STARTTLS extension (SmtpTransport::starttls_relay). This is the second best option after the previous TLS wrapped option", smtp_host);
}
Ok(false) => {
tracing::error!(
"Couldn't connect to {} via STARTTLS. No more information is available",
smtp_host
);
}
Err(err) => {
tracing::error!(err = %err, "Couldn't connect to {} via STARTTLS", smtp_host);
}
}
}
println!();
// Plaintext connection (very insecure)
{
tracing::info!(
"Trying to establish a plaintext connection to {}",
smtp_host
);
let transport = SmtpTransport::builder_dangerous(&smtp_host)
.timeout(Some(Duration::from_secs(10)))
.build();
match transport.test_connection() {
Ok(true) => {
tracing::info!("Successfully connected to {} via a plaintext connection. This option is very insecure and shouldn't be used on the public internet (SmtpTransport::builder_dangerous)", smtp_host);
}
Ok(false) => {
tracing::error!(
"Couldn't connect to {} via a plaintext connection. No more information is available",
smtp_host
);
}
Err(err) => {
tracing::error!(err = %err, "Couldn't connect to {} via a plaintext connection", smtp_host);
}
}
}
}

View File

@@ -29,16 +29,12 @@ fn main() {
MultiPart::alternative() // This is composed of two parts. MultiPart::alternative() // This is composed of two parts.
.singlepart( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(header::ContentType( .header(header::ContentType::TEXT_PLAIN)
"text/plain; charset=utf8".parse().unwrap(),
))
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback. .body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
) )
.singlepart( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(header::ContentType( .header(header::ContentType::TEXT_HTML)
"text/html; charset=utf8".parse().unwrap(),
))
.body(String::from(html)), .body(String::from(html)),
), ),
) )

View File

@@ -38,16 +38,12 @@ fn main() {
MultiPart::alternative() // This is composed of two parts. MultiPart::alternative() // This is composed of two parts.
.singlepart( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(header::ContentType( .header(header::ContentType::TEXT_PLAIN)
"text/plain; charset=utf8".parse().unwrap(),
))
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback. .body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
) )
.singlepart( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(header::ContentType( .header(header::ContentType::TEXT_HTML)
"text/html; charset=utf8".parse().unwrap(),
))
.body(html.into_string()), .body(html.into_string()),
), ),
) )

View File

@@ -1,4 +1,4 @@
use lettre::{Message, SmtpTransport, Transport}; use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@@ -8,6 +8,7 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
@@ -17,6 +18,6 @@ fn main() {
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

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

View File

@@ -1,4 +1,7 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@@ -8,10 +11,11 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS // Open a remote connection to gmail using STARTTLS
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com") let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
@@ -22,6 +26,6 @@ fn main() {
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -1,4 +1,7 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@@ -8,10 +11,11 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com") let mailer = SmtpTransport::relay("smtp.gmail.com")
@@ -22,6 +26,6 @@ fn main() {
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -1,37 +0,0 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio02_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Executor,
Tokio02Transport,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
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 async year")
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -1,37 +0,0 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio02_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Executor,
Tokio02Transport,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
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 async year")
.body(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -1,12 +1,11 @@
// This line is only to make it compile from lettre's examples folder, // This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio. // since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code. // Won't be needed in user's code.
use tokio1_crate as tokio;
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Executor, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
Tokio1Transport, AsyncTransport, Message, Tokio1Executor,
}; };
use tokio1_crate as tokio;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -17,10 +16,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS // Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<Tokio1Executor> = let mailer: AsyncSmtpTransport<Tokio1Executor> =
@@ -32,6 +32,6 @@ async fn main() {
// Send the email // Send the email
match mailer.send(email).await { match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -1,12 +1,11 @@
// This line is only to make it compile from lettre's examples folder, // This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio. // since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code. // Won't be needed in user's code.
use tokio1_crate as tokio;
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Executor, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
Tokio1Transport, AsyncTransport, Message, Tokio1Executor,
}; };
use tokio1_crate as tokio;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -17,10 +16,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio1Executor> = let mailer: AsyncSmtpTransport<Tokio1Executor> =
@@ -32,6 +32,6 @@ async fn main() {
// Send the email // Send the email
match mailer.send(email).await { match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

3
rustfmt.toml Normal file
View File

@@ -0,0 +1,3 @@
format_code_in_doc_comments = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@@ -1,6 +1,3 @@
#[cfg(feature = "builder")]
use std::convert::TryFrom;
use super::Address; use super::Address;
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
use crate::message::header::{self, Headers}; use crate::message::header::{self, Headers};
@@ -27,15 +24,12 @@ impl Envelope {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use std::str::FromStr; /// # use lettre::address::{Address, Envelope};
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// let sender = Address::from_str("from@email.com")?; /// let sender = "sender@email.com".parse::<Address>()?;
/// let recipients = vec![Address::from_str("to@email.com")?]; /// let recipients = vec!["to@email.com".parse::<Address>()?];
/// ///
/// let envelope = Envelope::new(Some(sender), recipients); /// let envelope = Envelope::new(Some(sender), recipients);
/// # Ok(()) /// # Ok(())
@@ -59,15 +53,12 @@ impl Envelope {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use std::str::FromStr; /// # use lettre::address::{Address, Envelope};
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// let sender = Address::from_str("from@email.com")?; /// let sender = "from@email.com".parse::<Address>()?;
/// let recipients = vec![Address::from_str("to@email.com")?]; /// let recipients = vec!["to@email.com".parse::<Address>()?];
/// ///
/// let envelope = Envelope::new(Some(sender), recipients.clone())?; /// let envelope = Envelope::new(Some(sender), recipients.clone())?;
/// assert_eq!(envelope.to(), recipients.as_slice()); /// assert_eq!(envelope.to(), recipients.as_slice());
@@ -82,15 +73,12 @@ impl Envelope {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use std::str::FromStr; /// # use lettre::address::{Address, Envelope};
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// let sender = Address::from_str("from@email.com")?; /// let sender = "from@email.com".parse::<Address>()?;
/// let recipients = vec![Address::from_str("to@email.com")?]; /// let recipients = vec!["to@email.com".parse::<Address>()?];
/// ///
/// let envelope = Envelope::new(Some(sender), recipients.clone())?; /// let envelope = Envelope::new(Some(sender), recipients.clone())?;
/// assert!(envelope.from().is_some()); /// assert!(envelope.from().is_some());
@@ -104,6 +92,7 @@ impl Envelope {
self.reverse_path.as_ref() self.reverse_path.as_ref()
} }
#[cfg(feature = "smtp-transport")]
/// Check if any of the addresses in the envelope contains non-ascii chars /// Check if any of the addresses in the envelope contains non-ascii chars
pub(crate) fn has_non_ascii_addresses(&self) -> bool { pub(crate) fn has_non_ascii_addresses(&self) -> bool {
self.reverse_path self.reverse_path
@@ -120,15 +109,16 @@ impl TryFrom<&Headers> for Envelope {
fn try_from(headers: &Headers) -> Result<Self, Self::Error> { fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
let from = match headers.get::<header::Sender>() { let from = match headers.get::<header::Sender>() {
// If there is a Sender, use it // If there is a Sender, use it
Some(header::Sender(a)) => Some(a.email.clone()), Some(sender) => Some(Mailbox::from(sender).email),
// ... else try From // ... else try From
None => match headers.get::<header::From>() { None => match headers.get::<header::From>() {
Some(header::From(a)) => { Some(header::From(a)) => {
let from: Vec<Mailbox> = a.clone().into(); let mut from: Vec<Mailbox> = a.into();
if from.len() > 1 { if from.len() > 1 {
return Err(Error::TooManyFrom); return Err(Error::TooManyFrom);
} }
Some(from[0].email.clone()) let from = from.pop().expect("From header has 1 Mailbox");
Some(from.email)
} }
None => None, None => None,
}, },
@@ -136,18 +126,16 @@ impl TryFrom<&Headers> for Envelope {
fn add_addresses_from_mailboxes( fn add_addresses_from_mailboxes(
addresses: &mut Vec<Address>, addresses: &mut Vec<Address>,
mailboxes: Option<&Mailboxes>, mailboxes: Option<Mailboxes>,
) { ) {
if let Some(mailboxes) = mailboxes { if let Some(mailboxes) = mailboxes {
for mailbox in mailboxes.iter() { addresses.extend(mailboxes.into_iter().map(|mb| mb.email));
addresses.push(mailbox.email.clone());
}
} }
} }
let mut to = vec![]; let mut to = vec![];
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| &h.0)); add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| &h.0)); add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| &h.0)); add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| h.0));
Self::new(from, to) Self::new(from, to)
} }

View File

@@ -1,10 +1,6 @@
//! Representation of an email address //! Representation of an email address
use idna::domain_to_ascii;
use once_cell::sync::Lazy;
use regex::Regex;
use std::{ use std::{
convert::{TryFrom, TryInto},
error::Error, error::Error,
ffi::OsStr, ffi::OsStr,
fmt::{Display, Formatter, Result as FmtResult}, fmt::{Display, Formatter, Result as FmtResult},
@@ -12,21 +8,27 @@ use std::{
str::FromStr, str::FromStr,
}; };
use email_address::EmailAddress;
use idna::domain_to_ascii;
/// Represents an email address with a user and a domain name. /// Represents an email address with a user and a domain name.
/// ///
/// This type contains email in canonical form (_user@domain.tld_). /// This type contains email in canonical form (_user@domain.tld_).
/// ///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/). /// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
/// ///
/// # Examples /// # Examples
/// ///
/// You can create an `Address` from a user and a domain: /// You can create an `Address` from a user and a domain:
/// ///
/// ``` /// ```
/// # use lettre::Address; /// use lettre::Address;
///
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?; /// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.user(), "user");
/// assert_eq!(address.domain(), "email.com");
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -34,11 +36,13 @@ use std::{
/// You can also create an `Address` from a string literal by parsing it: /// You can also create an `Address` from a string literal by parsing it:
/// ///
/// ``` /// ```
/// use std::str::FromStr; /// use lettre::Address;
/// # use lettre::Address; ///
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::from_str("example@email.com")?; /// let address = "user@email.com".parse::<Address>()?;
/// assert_eq!(address.user(), "user");
/// assert_eq!(address.domain(), "email.com");
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -50,42 +54,6 @@ pub struct Address {
at_start: usize, at_start: usize,
} }
impl<U, D> TryFrom<(U, D)> for Address
where
U: AsRef<str>,
D: AsRef<str>,
{
type Error = AddressError;
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
let user = user.as_ref();
Address::check_user(user)?;
let domain = domain.as_ref();
Address::check_domain(domain)?;
let serialized = format!("{}@{}", user, domain);
Ok(Address {
serialized,
at_start: user.len(),
})
}
}
// Regex from the specs
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
// It will mark esoteric email addresses like quoted string as invalid
static USER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
)
.unwrap()
});
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
impl Address { impl Address {
/// Creates a new email address from a user and domain. /// Creates a new email address from a user and domain.
/// ///
@@ -96,8 +64,8 @@ impl Address {
/// ///
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?; /// let address = Address::new("user", "email.com")?;
/// let expected: Address = "example@email.com".parse()?; /// let expected = "user@email.com".parse::<Address>()?;
/// assert_eq!(expected, address); /// assert_eq!(expected, address);
/// # Ok(()) /// # Ok(())
/// # } /// # }
@@ -115,8 +83,8 @@ impl Address {
/// ///
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?; /// let address = Address::new("user", "email.com")?;
/// assert_eq!("example", address.user()); /// assert_eq!(address.user(), "user");
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -133,8 +101,8 @@ impl Address {
/// ///
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?; /// let address = Address::new("user", "email.com")?;
/// assert_eq!("email.com", address.domain()); /// assert_eq!(address.domain(), "email.com");
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -143,7 +111,7 @@ impl Address {
} }
pub(super) fn check_user(user: &str) -> Result<(), AddressError> { pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
if USER_RE.is_match(user) { if EmailAddress::is_valid_local_part(user) {
Ok(()) Ok(())
} else { } else {
Err(AddressError::InvalidUser) Err(AddressError::InvalidUser)
@@ -159,21 +127,25 @@ impl Address {
} }
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> { fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
if DOMAIN_RE.is_match(domain) { // Domain
if EmailAddress::is_valid_domain(domain) {
return Ok(()); return Ok(());
} }
if let Some(caps) = LITERAL_RE.captures(domain) { // IP
if let Some(cap) = caps.get(1) { let ip = domain
if cap.as_str().parse::<IpAddr>().is_ok() { .strip_prefix('[')
.and_then(|ip| ip.strip_suffix(']'))
.unwrap_or(domain);
if ip.parse::<IpAddr>().is_ok() {
return Ok(()); return Ok(());
} }
}
}
Err(AddressError::InvalidDomain) Err(AddressError::InvalidDomain)
} }
#[cfg(feature = "smtp-transport")]
/// Check if the address contains non-ascii chars /// Check if the address contains non-ascii chars
pub(super) fn is_ascii(&self) -> bool { pub(super) fn is_ascii(&self) -> bool {
self.serialized.is_ascii() self.serialized.is_ascii()
@@ -190,19 +162,48 @@ impl FromStr for Address {
type Err = AddressError; type Err = AddressError;
fn from_str(val: &str) -> Result<Self, AddressError> { fn from_str(val: &str) -> Result<Self, AddressError> {
let mut parts = val.rsplitn(2, '@'); let at_start = check_address(val)?;
let domain = parts.next().ok_or(AddressError::MissingParts)?;
let user = parts.next().ok_or(AddressError::MissingParts)?;
Address::check_user(user)?;
Address::check_domain(domain)?;
Ok(Address { Ok(Address {
serialized: val.into(), serialized: val.into(),
at_start,
})
}
}
impl<U, D> TryFrom<(U, D)> for Address
where
U: AsRef<str>,
D: AsRef<str>,
{
type Error = AddressError;
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
let user = user.as_ref();
Address::check_user(user)?;
let domain = domain.as_ref();
Address::check_domain(domain)?;
let serialized = format!("{user}@{domain}");
Ok(Address {
serialized,
at_start: user.len(), at_start: user.len(),
}) })
} }
} }
impl TryFrom<String> for Address {
type Error = AddressError;
fn try_from(serialized: String) -> Result<Self, AddressError> {
let at_start = check_address(&serialized)?;
Ok(Address {
serialized,
at_start,
})
}
}
impl AsRef<str> for Address { impl AsRef<str> for Address {
fn as_ref(&self) -> &str { fn as_ref(&self) -> &str {
&self.serialized &self.serialized
@@ -215,14 +216,30 @@ impl AsRef<OsStr> for Address {
} }
} }
#[derive(Debug, PartialEq, Clone, Copy)] fn check_address(val: &str) -> Result<usize, AddressError> {
let mut parts = val.rsplitn(2, '@');
let domain = parts.next().ok_or(AddressError::MissingParts)?;
let user = parts.next().ok_or(AddressError::MissingParts)?;
Address::check_user(user)?;
Address::check_domain(domain)?;
Ok(user.len())
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
/// Errors in email addresses parsing /// Errors in email addresses parsing
pub enum AddressError { pub enum AddressError {
/// Missing domain or user
MissingParts, MissingParts,
/// Unbalanced angle bracket
Unbalanced, Unbalanced,
/// Invalid email user
InvalidUser, InvalidUser,
/// Invalid email domain
InvalidDomain, InvalidDomain,
InvalidUtf8b, /// Invalid input found
InvalidInput,
} }
impl Error for AddressError {} impl Error for AddressError {}
@@ -234,7 +251,7 @@ impl Display for AddressError {
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"), AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
AddressError::InvalidUser => f.write_str("Invalid email user"), AddressError::InvalidUser => f.write_str("Invalid email user"),
AddressError::InvalidDomain => f.write_str("Invalid email domain"), AddressError::InvalidDomain => f.write_str("Invalid email domain"),
AddressError::InvalidUtf8b => f.write_str("Invalid UTF8b data"), AddressError::InvalidInput => f.write_str("Invalid input"),
} }
} }
} }
@@ -244,7 +261,7 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn parse_address() { fn ascii_address() {
let addr_str = "something@example.com"; let addr_str = "something@example.com";
let addr = Address::from_str(addr_str).unwrap(); let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "example.com").unwrap(); let addr2 = Address::new("something", "example.com").unwrap();
@@ -254,4 +271,36 @@ mod tests {
assert_eq!(addr2.user(), "something"); assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "example.com"); assert_eq!(addr2.domain(), "example.com");
} }
#[test]
fn ascii_address_ipv4() {
let addr_str = "something@1.1.1.1";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "1.1.1.1").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "1.1.1.1");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "1.1.1.1");
}
#[test]
fn ascii_address_ipv6() {
let addr_str = "something@[2606:4700:4700::1111]";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "[2606:4700:4700::1111]").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "[2606:4700:4700::1111]");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "[2606:4700:4700::1111]");
}
#[test]
fn check_parts() {
assert!(Address::check_user("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err());
assert!(
Address::check_domain("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com").is_err()
);
}
} }

12
src/base64.rs Normal file
View File

@@ -0,0 +1,12 @@
use ::base64::{
engine::{general_purpose::STANDARD, Engine},
DecodeError,
};
pub(crate) fn encode<T: AsRef<[u8]>>(input: T) -> String {
STANDARD.encode(input)
}
pub(crate) fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
STANDARD.decode(input)
}

View File

@@ -1,38 +1,71 @@
use async_trait::async_trait; use std::fmt::Debug;
#[cfg(feature = "smtp-transport")]
use std::future::Future;
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
use std::io::Result as IoResult; use std::io::Result as IoResult;
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
use std::path::Path; use std::path::Path;
#[cfg(feature = "smtp-transport")]
use std::time::Duration;
use async_trait::async_trait;
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
use futures_util::future::BoxFuture;
#[cfg(all( #[cfg(all(
feature = "smtp-transport", feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1") any(feature = "tokio1", feature = "async-std1")
))] ))]
use crate::transport::smtp::client::AsyncSmtpConnection; use crate::transport::smtp::client::AsyncSmtpConnection;
#[cfg(all( #[cfg(all(
feature = "smtp-transport", feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1") any(feature = "tokio1", feature = "async-std1")
))] ))]
use crate::transport::smtp::client::Tls; use crate::transport::smtp::client::Tls;
#[cfg(all( #[cfg(all(
feature = "smtp-transport", feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1") any(feature = "tokio1", feature = "async-std1")
))] ))]
use crate::transport::smtp::extension::ClientId; use crate::transport::smtp::extension::ClientId;
#[cfg(all( #[cfg(all(
feature = "smtp-transport", feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1") any(feature = "tokio1", feature = "async-std1")
))] ))]
use crate::transport::smtp::Error; use crate::transport::smtp::Error;
/// Async executor abstraction trait
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[async_trait] #[async_trait]
pub trait Executor: Send + Sync + private::Sealed { pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
#[cfg(feature = "smtp-transport")]
type Handle: SpawnHandle;
#[cfg(feature = "smtp-transport")]
type Sleep: Future<Output = ()> + Send + 'static;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle
where
F: Future<Output = ()> + Send + 'static,
F::Output: Send + 'static;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep;
#[doc(hidden)] #[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
port: u16, port: u16,
timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls: &Tls, tls: &Tls,
) -> Result<AsyncSmtpConnection, Error>; ) -> Result<AsyncSmtpConnection, Error>;
@@ -46,99 +79,83 @@ pub trait Executor: Send + Sync + private::Sealed {
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>; async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
} }
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
pub struct Tokio02Executor;
#[async_trait]
#[cfg(feature = "tokio02")]
impl Executor for Tokio02Executor {
#[doc(hidden)] #[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( #[async_trait]
hostname: &str, pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
port: u16, async fn shutdown(self);
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio02(hostname, port, hello_name, tls_parameters)
.await?;
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
match tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
}
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
tokio02_crate::fs::read(path).await
}
#[doc(hidden)]
#[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
tokio02_crate::fs::write(path, contents).await
}
} }
/// Async [`Executor`] using `tokio` `1.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)] #[allow(missing_copy_implementations)]
#[non_exhaustive] #[non_exhaustive]
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))] #[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
#[derive(Debug)]
pub struct Tokio1Executor; pub struct Tokio1Executor;
#[async_trait] #[async_trait]
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
impl Executor for Tokio1Executor { impl Executor for Tokio1Executor {
#[doc(hidden)] #[cfg(feature = "smtp-transport")]
type Handle = tokio1_crate::task::JoinHandle<()>;
#[cfg(feature = "smtp-transport")]
type Sleep = tokio1_crate::time::Sleep;
#[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle
where
F: Future<Output = ()> + Send + 'static,
F::Output: Send + 'static,
{
tokio1_crate::spawn(fut)
}
#[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep {
tokio1_crate::time::sleep(duration)
}
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
port: u16, port: u16,
timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls: &Tls, tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
let tls_parameters = match tls { let tls_parameters = match tls {
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-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, _ => None,
}; };
#[allow(unused_mut)] #[allow(unused_mut)]
let mut conn = let mut conn = AsyncSmtpConnection::connect_tokio1(
AsyncSmtpConnection::connect_tokio1(hostname, port, hello_name, tls_parameters).await?; (hostname, port),
timeout,
hello_name,
tls_parameters,
None,
)
.await?;
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
match tls { match tls {
Tls::Opportunistic(ref tls_parameters) => { Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() { 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) => { Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
_ => (), _ => (),
} }
@@ -146,56 +163,95 @@ impl Executor for Tokio1Executor {
Ok(conn) Ok(conn)
} }
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> { async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
tokio1_crate::fs::read(path).await tokio1_crate::fs::read(path).await
} }
#[doc(hidden)]
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> { async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
tokio1_crate::fs::write(path, contents).await tokio1_crate::fs::write(path, contents).await
} }
} }
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
#[async_trait]
impl SpawnHandle for tokio1_crate::task::JoinHandle<()> {
async fn shutdown(self) {
self.abort();
}
}
/// Async [`Executor`] using `async-std` `1.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)] #[allow(missing_copy_implementations)]
#[non_exhaustive] #[non_exhaustive]
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))] #[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
#[derive(Debug)]
pub struct AsyncStd1Executor; pub struct AsyncStd1Executor;
#[async_trait] #[async_trait]
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
impl Executor for AsyncStd1Executor { impl Executor for AsyncStd1Executor {
#[doc(hidden)] #[cfg(feature = "smtp-transport")]
type Handle = async_std::task::JoinHandle<()>;
#[cfg(feature = "smtp-transport")]
type Sleep = BoxFuture<'static, ()>;
#[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle
where
F: Future<Output = ()> + Send + 'static,
F::Output: Send + 'static,
{
async_std::task::spawn(fut)
}
#[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep {
let fut = async_std::task::sleep(duration);
Box::pin(fut)
}
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
port: u16, port: u16,
timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls: &Tls, tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
let tls_parameters = match tls { let tls_parameters = match tls {
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-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, _ => None,
}; };
#[allow(unused_mut)] #[allow(unused_mut)]
let mut conn = let mut conn = AsyncSmtpConnection::connect_asyncstd1(
AsyncSmtpConnection::connect_asyncstd1(hostname, port, hello_name, tls_parameters) (hostname, port),
timeout,
hello_name,
tls_parameters,
)
.await?; .await?;
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))] #[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
match tls { match tls {
Tls::Opportunistic(ref tls_parameters) => { Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() { 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) => { Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
_ => (), _ => (),
} }
@@ -203,30 +259,37 @@ impl Executor for AsyncStd1Executor {
Ok(conn) Ok(conn)
} }
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> { async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
async_std::fs::read(path).await async_std::fs::read(path).await
} }
#[doc(hidden)]
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> { async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
async_std::fs::write(path, contents).await async_std::fs::write(path, contents).await
} }
} }
mod private { #[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
use super::*; #[async_trait]
impl SpawnHandle for async_std::task::JoinHandle<()> {
async fn shutdown(self) {
self.cancel().await;
}
}
mod private {
pub trait Sealed {} pub trait Sealed {}
#[cfg(feature = "tokio02")]
impl Sealed for Tokio02Executor {}
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
impl Sealed for Tokio1Executor {} impl Sealed for super::Tokio1Executor {}
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
impl Sealed for AsyncStd1Executor {} impl Sealed for super::AsyncStd1Executor {}
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
impl Sealed for tokio1_crate::task::JoinHandle<()> {}
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
impl Sealed for async_std::task::JoinHandle<()> {}
} }

View File

@@ -4,33 +4,112 @@
//! * Pluggable email transports //! * Pluggable email transports
//! * Unicode support //! * Unicode support
//! * Secure defaults //! * Secure defaults
//! * Async support
//! //!
//! Lettre requires Rust 1.45 or newer. //! Lettre requires Rust 1.70 or newer.
//! //!
//! ## Optional features //! ## Features
//!
//! This section lists each lettre feature and briefly explains it.
//! More info about each module can be found in the corresponding module page.
//!
//! Features with `📫` near them are enabled by default.
//!
//! ### Typed message builder
//!
//! _Strongly typed [`message`] builder_
//!
//! * **builder** 📫: Enable the [`Message`] builder
//! * **hostname** 📫: Try to use the actual system hostname in the `Message-ID` header
//!
//! ### SMTP transport
//!
//! _Send emails using [`SMTP`]_
//!
//! * **smtp-transport** 📫: Enable the SMTP transport
//! * **pool** 📫: Connection pool for SMTP transport
//! * **hostname** 📫: Try to use the actual system hostname for the SMTP `CLIENTID`
//!
//! #### SMTP over TLS via the native-tls crate
//!
//! _Secure SMTP connections using TLS from the `native-tls` crate_
//!
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
//!
//! * **native-tls** 📫: TLS support for the synchronous version of the API
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: native-tls isn't supported with `async-std`
//!
//! #### SMTP over TLS via the boring crate (Boring TLS)
//!
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
//!
//! * **boring-tls**: TLS support for the synchronous version of the API
//! * **tokio1-boring-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: boring-tls isn't supported with `async-std`
//!
//! #### SMTP over TLS via the rustls crate
//!
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
//!
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
//!
//! * **rustls-tls**: TLS support for the synchronous version of the API
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
//!
//! ### Sendmail transport
//!
//! _Send emails using the [`sendmail`] command_
//!
//! * **sendmail-transport**: Enable the `sendmail` transport
//!
//! ### File transport
//!
//! _Save emails as an `.eml` [`file`]_
//!
//! * **file-transport**: Enable the file transport (saves emails into an `.eml` file)
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file (additionally saves envelopes into a `.json` file)
//!
//! ### Async execution runtimes
//!
//! _Use [tokio] or [async-std] as an async execution runtime for sending emails_
//!
//! The correct runtime version must be chosen in order for lettre to work correctly.
//! For example, when sending emails from a Tokio 1.x context, the Tokio 1.x executor
//! ([`Tokio1Executor`]) must be used. Using a different version (for example Tokio 0.2.x),
//! or async-std, would result in a runtime panic.
//!
//! * **tokio1**: Allow to asynchronously send emails using [Tokio 1.x]
//! * **async-std1**: Allow to asynchronously send emails using [async-std 1.x]
//!
//! NOTE: native-tls isn't supported with `async-std`
//!
//! ### Misc features
//!
//! _Additional features_
//! //!
//! * **builder**: Message builder
//! * **file-transport**: Transport that write messages into a file
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file
//! * **smtp-transport**: Transport over SMTP
//! * **sendmail-transport**: Transport over SMTP
//! * **rustls-tls**: TLS support with the `rustls` crate
//! * **native-tls**: TLS support with the `native-tls` crate
//! * **tokio02**: Allow to asyncronously send emails using tokio 0.2.x
//! * **tokio02-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.2
//! * **tokio02-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.2
//! * **tokio1**: Allow to asyncronously send emails using tokio 1.x
//! * **tokio1-rustls-tls**: Async TLS support with the `rustls` crate using tokio 1.x
//! * **tokio1-native-tls**: Async TLS support with the `native-tls` crate using tokio 1.x
//! * **async-std1**: Allow to asynchronously send emails using async-std 1.x
//! * NOTE: native-tls isn't supported with async-std at the moment
//! * **async-std1-rustls-tls**: Async TLS support with the `rustls` crate using async-std 1.x
//! * **r2d2**: Connection pool for SMTP transport
//! * **tracing**: Logging using the `tracing` crate
//! * **serde**: Serialization/Deserialization of entities //! * **serde**: Serialization/Deserialization of entities
//! * **hostname**: Ability to try to use actual hostname in SMTP transaction //! * **tracing**: Logging using the `tracing` crate
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
//! * **dkim**: Add support for signing email with DKIM
//!
//! [`SMTP`]: crate::transport::smtp
//! [`sendmail`]: crate::transport::sendmail
//! [`file`]: crate::transport::file
//! [`ContentType`]: crate::message::header::ContentType
//! [tokio]: https://docs.rs/tokio/1
//! [async-std]: https://docs.rs/async-std/1
//! [ring]: https://github.com/briansmith/ring#ring
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
//! [Tokio 1.x]: https://docs.rs/tokio/1
//! [async-std 1.x]: https://docs.rs/async-std/1
//! [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.0-beta.1")] #![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.4")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")] #![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")] #![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -40,89 +119,153 @@
trivial_numeric_casts, trivial_numeric_casts,
unstable_features, unstable_features,
unused_import_braces, unused_import_braces,
rust_2018_idioms rust_2018_idioms,
clippy::string_add,
clippy::string_add_assign,
clippy::clone_on_ref_ptr,
clippy::verbose_file_reads,
clippy::unnecessary_self_imports,
clippy::string_to_string,
clippy::mem_forget,
clippy::cast_lossless,
clippy::inefficient_to_string,
clippy::inline_always,
clippy::linkedlist,
clippy::macro_use_imports,
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))] #![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(not(lettre_ignore_tls_mismatch))]
mod compiletime_checks {
#[cfg(all(feature = "native-tls", feature = "boring-tls"))]
compile_error!("feature \"native-tls\" and feature \"boring-tls\" cannot be enabled at the same time, otherwise
the executable will fail to link.");
#[cfg(all(
feature = "tokio1",
feature = "native-tls",
not(feature = "tokio1-native-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "tokio1",
feature = "rustls-tls",
not(feature = "tokio1-rustls-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `rustls-tls` features, but the `tokio1-rustls-tls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "tokio1",
feature = "boring-tls",
not(feature = "tokio1-boring-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `boring-tls` features, but the `tokio1-boring-tls` feature hasn't been turned on.
If you'd like to use `boring-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
/*
#[cfg(all(
feature = "async-std1",
feature = "native-tls",
not(feature = "async-std1-native-tls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the `async-std1-native-tls` feature hasn't been turned on.
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
*/
#[cfg(all(
feature = "async-std1",
feature = "native-tls",
not(feature = "async-std1-native-tls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "async-std1",
feature = "rustls-tls",
not(feature = "async-std1-rustls-tls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `rustls-tls` features, but the `async-std1-rustls-tls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
}
pub mod address; pub mod address;
#[cfg(any(feature = "smtp-transport", feature = "dkim"))]
mod base64;
pub mod error; pub mod error;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod executor; mod executor;
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))] #[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
pub mod message; pub mod message;
pub mod transport; pub mod transport;
#[cfg(feature = "builder")] use std::error::Error as StdError;
#[macro_use]
extern crate hyperx;
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
pub use self::executor::AsyncStd1Executor; pub use self::executor::AsyncStd1Executor;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::executor::Executor; pub use self::executor::Executor;
#[cfg(feature = "tokio02")]
pub use self::executor::Tokio02Executor;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub use self::executor::Tokio1Executor; pub use self::executor::Tokio1Executor;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[doc(inline)]
pub use self::transport::AsyncTransport; pub use self::transport::AsyncTransport;
pub use crate::address::Address; pub use crate::address::Address;
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
#[doc(inline)]
pub use crate::message::Message; pub use crate::message::Message;
#[cfg(all( #[cfg(all(
feature = "file-transport", feature = "file-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1") any(feature = "tokio1", feature = "async-std1")
))] ))]
#[doc(inline)]
pub use crate::transport::file::AsyncFileTransport; pub use crate::transport::file::AsyncFileTransport;
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
#[doc(inline)]
pub use crate::transport::file::FileTransport; pub use crate::transport::file::FileTransport;
#[cfg(all( #[cfg(all(
feature = "sendmail-transport", feature = "sendmail-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1") any(feature = "tokio1", feature = "async-std1")
))] ))]
#[doc(inline)]
pub use crate::transport::sendmail::AsyncSendmailTransport; pub use crate::transport::sendmail::AsyncSendmailTransport;
#[cfg(feature = "sendmail-transport")] #[cfg(feature = "sendmail-transport")]
#[doc(inline)]
pub use crate::transport::sendmail::SendmailTransport; pub use crate::transport::sendmail::SendmailTransport;
#[cfg(all( #[cfg(all(
feature = "smtp-transport", feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1") any(feature = "tokio1", feature = "async-std1")
))] ))]
pub use crate::transport::smtp::AsyncSmtpTransport; pub use crate::transport::smtp::AsyncSmtpTransport;
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::SmtpTransport;
#[doc(inline)]
pub use crate::transport::Transport; pub use crate::transport::Transport;
use crate::{address::Envelope, error::Error}; use crate::{address::Envelope, error::Error};
#[doc(hidden)] pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
#[allow(deprecated)]
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
pub use crate::transport::smtp::AsyncStd1Connector;
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::SmtpTransport;
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(all(feature = "smtp-transport", feature = "tokio02"))]
pub use crate::transport::smtp::Tokio02Connector;
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
pub use crate::transport::smtp::Tokio1Connector;
#[doc(hidden)]
#[cfg(feature = "async-std1")]
pub use crate::transport::AsyncStd1Transport;
#[doc(hidden)]
#[cfg(feature = "tokio02")]
pub use crate::transport::Tokio02Transport;
#[doc(hidden)]
#[cfg(feature = "tokio1")]
pub use crate::transport::Tokio1Transport;
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
mod test { mod test {
use super::*; use super::*;
use crate::message::{header, Mailbox, Mailboxes}; use crate::message::{header, header::Headers, Mailbox, Mailboxes};
use hyperx::header::Headers;
use std::convert::TryFrom;
#[test] #[test]
fn envelope_from_headers() { fn envelope_from_headers() {
@@ -150,9 +293,9 @@ mod test {
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap()); let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set(header::From(from)); headers.set(header::From::from(from));
headers.set(header::Sender(sender)); headers.set(header::Sender::from(sender));
headers.set(header::To(to)); headers.set(header::To::from(to));
assert_eq!( assert_eq!(
Envelope::try_from(&headers).unwrap(), Envelope::try_from(&headers).unwrap(),
@@ -170,8 +313,8 @@ mod test {
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap()); let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set(header::From(from)); headers.set(header::From::from(from));
headers.set(header::Sender(sender)); headers.set(header::Sender::from(sender));
assert!(Envelope::try_from(&headers).is_err(),); assert!(Envelope::try_from(&headers).is_err(),);
} }

145
src/message/attachment.rs Normal file
View File

@@ -0,0 +1,145 @@
use crate::message::{
header::{self, ContentType},
IntoBody, SinglePart,
};
/// `SinglePart` builder for attachments
///
/// Allows building attachment parts easily.
#[derive(Clone)]
pub struct Attachment {
disposition: Disposition,
}
#[derive(Clone)]
enum Disposition {
/// File name
Attached(String),
/// Content id
Inline(String),
}
impl Attachment {
/// Create a new attachment
///
/// This attachment will be displayed as a normal attachment,
/// with the chosen `filename` appearing as the file name.
///
/// ```rust
/// # use std::error::Error;
/// use std::fs;
///
/// use lettre::message::{header::ContentType, Attachment};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let filename = String::from("invoice.pdf");
/// # if false {
/// let filebody = fs::read("invoice.pdf")?;
/// # }
/// # let filebody = fs::read("docs/lettre.png")?;
/// let content_type = ContentType::parse("application/pdf").unwrap();
/// let attachment = Attachment::new(filename).body(filebody, content_type);
///
/// // The document `attachment` will show up as a normal attachment.
/// # Ok(())
/// # }
/// ```
pub fn new(filename: String) -> Self {
Attachment {
disposition: Disposition::Attached(filename),
}
}
/// Create a new inline attachment
///
/// This attachment should be displayed inline into the message
/// body:
///
/// ```html
/// <img src="cid:123">
/// ```
///
///
/// ```rust
/// # use std::error::Error;
/// use std::fs;
///
/// use lettre::message::{header::ContentType, Attachment};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let content_id = String::from("123");
/// # if false {
/// let filebody = fs::read("image.jpg")?;
/// # }
/// # let filebody = fs::read("docs/lettre.png")?;
/// let content_type = ContentType::parse("image/jpeg").unwrap();
/// let attachment = Attachment::new_inline(content_id).body(filebody, content_type);
///
/// // The image `attachment` will display inline into the email.
/// # Ok(())
/// # }
/// ```
pub fn new_inline(content_id: String) -> Self {
Attachment {
disposition: Disposition::Inline(content_id),
}
}
/// Build the attachment into a [`SinglePart`] which can then be used to build the rest of the email
///
/// Look at the [Complex MIME body example](crate::message#complex-mime-body)
/// to see how [`SinglePart`] can be put into the email.
pub fn body<T: IntoBody>(self, content: T, content_type: ContentType) -> SinglePart {
let mut builder = SinglePart::builder();
builder = match self.disposition {
Disposition::Attached(filename) => {
builder.header(header::ContentDisposition::attachment(&filename))
}
Disposition::Inline(content_id) => builder
.header(header::ContentId::from(format!("<{content_id}>")))
.header(header::ContentDisposition::inline()),
};
builder = builder.header(content_type);
builder.body(content)
}
}
#[cfg(test)]
mod tests {
use crate::message::header::ContentType;
#[test]
fn attachment() {
let part = super::Attachment::new(String::from("test.txt")).body(
String::from("Hello world!"),
ContentType::parse("text/plain").unwrap(),
);
assert_eq!(
&String::from_utf8_lossy(&part.formatted()),
concat!(
"Content-Disposition: attachment; filename=\"test.txt\"\r\n",
"Content-Type: text/plain\r\n",
"Content-Transfer-Encoding: 7bit\r\n\r\n",
"Hello world!\r\n",
)
);
}
#[test]
fn attachment_inline() {
let part = super::Attachment::new_inline(String::from("id")).body(
String::from("Hello world!"),
ContentType::parse("text/plain").unwrap(),
);
assert_eq!(
&String::from_utf8_lossy(&part.formatted()),
concat!(
"Content-ID: <id>\r\n",
"Content-Disposition: inline\r\n",
"Content-Type: text/plain\r\n",
"Content-Transfer-Encoding: 7bit\r\n\r\n",
"Hello world!\r\n"
)
);
}
}

View File

@@ -1,7 +1,4 @@
use std::{ use std::{mem, ops::Deref};
io::{self, Write},
ops::Deref,
};
use crate::message::header::ContentTransferEncoding; use crate::message::header::ContentTransferEncoding;
@@ -18,7 +15,9 @@ pub struct Body {
/// makes for a more efficient `Content-Transfer-Encoding` to be chosen. /// makes for a more efficient `Content-Transfer-Encoding` to be chosen.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum MaybeString { pub enum MaybeString {
/// Binary data
Binary(Vec<u8>), Binary(Vec<u8>),
/// UTF-8 string
String(String), String(String),
} }
@@ -30,13 +29,16 @@ impl Body {
/// Automatically chooses the most efficient encoding between /// Automatically chooses the most efficient encoding between
/// `7bit`, `quoted-printable` and `base64`. /// `7bit`, `quoted-printable` and `base64`.
/// ///
/// If `String` is passed, line endings are converted to `CRLF`.
///
/// If `buf` is valid utf-8 a `String` should be supplied, as `String`s /// If `buf` is valid utf-8 a `String` should be supplied, as `String`s
/// can be encoded as `7bit` or `quoted-printable`, while `Vec<u8>` always /// can be encoded as `7bit` or `quoted-printable`, while `Vec<u8>` always
/// get encoded as `base64`. /// get encoded as `base64`.
pub fn new<B: Into<MaybeString>>(buf: B) -> Self { pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
let buf: MaybeString = buf.into(); let mut buf: MaybeString = buf.into();
let encoding = buf.encoding(); let encoding = buf.encoding(false);
buf.encode_crlf();
Self::new_impl(buf.into(), encoding) Self::new_impl(buf.into(), encoding)
} }
@@ -44,6 +46,8 @@ impl Body {
/// ///
/// [`Body::new`] is generally the better option. /// [`Body::new`] is generally the better option.
/// ///
/// If `String` is passed, line endings are converted to `CRLF`.
///
/// Returns an [`Err`] giving back the supplied `buf`, in case the chosen /// Returns an [`Err`] giving back the supplied `buf`, in case the chosen
/// encoding would have resulted into `buf` being encoded /// encoding would have resulted into `buf` being encoded
/// into an invalid body. /// into an invalid body.
@@ -51,12 +55,28 @@ impl Body {
buf: B, buf: B,
encoding: ContentTransferEncoding, encoding: ContentTransferEncoding,
) -> Result<Self, Vec<u8>> { ) -> Result<Self, Vec<u8>> {
let buf: MaybeString = buf.into(); let mut buf: MaybeString = buf.into();
if !buf.is_encoding_ok(encoding) { let best_encoding = buf.encoding(true);
let ok = match (encoding, best_encoding) {
(ContentTransferEncoding::SevenBit, ContentTransferEncoding::SevenBit) => true,
(
ContentTransferEncoding::EightBit,
ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit,
) => true,
(ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit, _) => false,
(
ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64
| ContentTransferEncoding::Binary,
_,
) => true,
};
if !ok {
return Err(buf.into()); return Err(buf.into());
} }
buf.encode_crlf();
Ok(Self::new_impl(buf.into(), encoding)) Ok(Self::new_impl(buf.into(), encoding))
} }
@@ -82,36 +102,13 @@ impl Body {
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable) Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
} }
ContentTransferEncoding::Base64 => { ContentTransferEncoding::Base64 => {
let base64_len = buf.len() * 4 / 3 + 4; let len = email_encoding::body::base64::encoded_len(buf.len());
let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
let mut out = Vec::with_capacity(base64_endings_len); let mut out = String::with_capacity(len);
{ email_encoding::body::base64::encode(&buf, &mut out)
let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH); .expect("encode body as base64");
let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
// TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails"); Self::dangerous_pre_encoded(out.into_bytes(), ContentTransferEncoding::Base64)
// modified Write::write_all to work around base64 crate bug
// TODO: remove once https://github.com/marshallpierce/rust-base64/issues/148 is fixed
{
let mut buf: &[u8] = buf.as_ref();
while !buf.is_empty() {
match writer.write(buf) {
Ok(0) => {
// ignore 0 writes
}
Ok(n) => {
buf = &buf[n..];
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => panic!("base64 encoding never fails: {}", e),
}
}
}
}
Self::dangerous_pre_encoded(out, ContentTransferEncoding::Base64)
} }
} }
} }
@@ -144,33 +141,28 @@ impl Body {
impl MaybeString { impl MaybeString {
/// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString` /// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
/// ///
/// If the `MaybeString` was created from a `String` composed only of US-ASCII /// The `binary` encoding is never returned
/// characters, with no lines longer than 1000 characters, then 7bit fn encoding(&self, supports_utf8: bool) -> ContentTransferEncoding {
/// encoding will be used, else quoted-printable will be chosen. use email_encoding::body::Encoding;
///
/// If the `MaybeString` was instead created from a `Vec<u8>`, base64 encoding is always let output = match self {
/// chosen. Self::String(s) => Encoding::choose(s.as_str(), supports_utf8),
/// Self::Binary(b) => Encoding::choose(b.as_slice(), supports_utf8),
/// `8bit` and `binary` encodings are never returned, as they may not be };
/// supported by all SMTP servers.
pub fn encoding(&self) -> ContentTransferEncoding { match output {
match &self { Encoding::SevenBit => ContentTransferEncoding::SevenBit,
Self::String(s) if is_7bit_encoded(s.as_ref()) => ContentTransferEncoding::SevenBit, Encoding::EightBit => ContentTransferEncoding::EightBit,
// TODO: consider when base64 would be a better option because of output size Encoding::QuotedPrintable => ContentTransferEncoding::QuotedPrintable,
Self::String(_) => ContentTransferEncoding::QuotedPrintable, Encoding::Base64 => ContentTransferEncoding::Base64,
Self::Binary(_) => ContentTransferEncoding::Base64,
} }
} }
/// Returns `true` if using `encoding` to encode this `MaybeString` /// Encode line endings to CRLF if the variant is `String`
/// would result into an invalid encoded body. fn encode_crlf(&mut self) {
fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool { match self {
match encoding { Self::String(string) => in_place_crlf_line_endings(string),
ContentTransferEncoding::SevenBit => is_7bit_encoded(&self), Self::Binary(_) => {}
ContentTransferEncoding::EightBit => is_8bit_encoded(&self),
ContentTransferEncoding::Binary
| ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64 => true,
} }
} }
} }
@@ -189,6 +181,7 @@ impl MaybeString {
/// **NOTE:** if using the specified `encoding` would result into a malformed /// **NOTE:** if using the specified `encoding` would result into a malformed
/// body, this will panic! /// body, this will panic!
pub trait IntoBody { pub trait IntoBody {
/// Encode as valid body
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body; fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body;
} }
@@ -255,76 +248,46 @@ impl Deref for MaybeString {
} }
} }
/// Checks whether it contains only US-ASCII characters, /// In place conversion to CRLF line endings
/// and no lines are longer than 1000 characters including the `\n` character. fn in_place_crlf_line_endings(string: &mut String) {
let indices = find_all_lf_char_indices(string);
for i in indices {
// this relies on `indices` being in reverse order
string.insert(i, '\r');
}
}
/// Find indices to all places where `\r` should be inserted
/// in order to make `s` have CRLF line endings
/// ///
/// Most efficient content encoding available /// The list is reversed, which is more efficient.
fn is_7bit_encoded(buf: &[u8]) -> bool { fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
buf.is_ascii() && !contains_too_long_lines(buf) let mut indices = Vec::new();
let mut found_lf = false;
for (i, c) in s.char_indices().rev() {
if mem::take(&mut found_lf) && c != '\r' {
// the previous character was `\n`, but this isn't a `\r`
indices.push(i + c.len_utf8());
} }
/// Checks that no lines are longer than 1000 characters, found_lf = c == '\n';
/// including the `\n` character.
/// NOTE: 8bit isn't supported by all SMTP servers.
fn is_8bit_encoded(buf: &[u8]) -> bool {
!contains_too_long_lines(buf)
} }
/// Checks if there are lines that are longer than 1000 characters, if found_lf {
/// including the `\n` character. // the first character is `\n`
fn contains_too_long_lines(buf: &[u8]) -> bool { indices.push(0);
buf.len() > 1000 && buf.split(|&b| b == b'\n').any(|line| line.len() > 999)
} }
const LINE_SEPARATOR: &[u8] = b"\r\n"; indices
const LINE_MAX_LENGTH: usize = 78 - LINE_SEPARATOR.len();
/// A `Write`r that inserts a line separator `\r\n` every `max_line_length` bytes.
struct LineWrappingWriter<'a, W> {
writer: &'a mut W,
current_line_length: usize,
max_line_length: usize,
}
impl<'a, W> LineWrappingWriter<'a, W> {
pub fn new(writer: &'a mut W, max_line_length: usize) -> Self {
Self {
writer,
current_line_length: 0,
max_line_length,
}
}
}
impl<'a, W> Write for LineWrappingWriter<'a, W>
where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let remaining_line_len = self.max_line_length - self.current_line_length;
let write_len = std::cmp::min(buf.len(), remaining_line_len);
self.writer.write_all(&buf[..write_len])?;
if remaining_line_len == write_len {
self.writer.write_all(LINE_SEPARATOR)?;
self.current_line_length = 0;
} else {
self.current_line_length += write_len;
}
Ok(write_len)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{Body, ContentTransferEncoding}; use pretty_assertions::assert_eq;
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
#[test] #[test]
fn seven_bit_detect() { fn seven_bit_detect() {
@@ -456,13 +419,10 @@ mod test {
#[test] #[test]
fn quoted_printable_detect() { fn quoted_printable_detect() {
let encoded = Body::new(String::from("Привет, мир!")); let encoded = Body::new(String::from("Questo messaggio è corto"));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!( assert_eq!(encoded.as_ref(), b"Questo messaggio =C3=A8 corto");
encoded.as_ref(),
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
);
} }
#[test] #[test]
@@ -494,14 +454,17 @@ mod test {
#[test] #[test]
fn quoted_printable_encode_line_wrap() { fn quoted_printable_encode_line_wrap() {
let encoded = Body::new(String::from("Текст письма в уникоде")); let encoded = Body::new(String::from(
"Se lo standard 📬 fosse stato più semplice avremmo finito molto prima.",
));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
println!("{}", std::str::from_utf8(encoded.as_ref()).unwrap());
assert_eq!( assert_eq!(
encoded.as_ref(), encoded.as_ref(),
concat!( concat!(
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n", "Se lo standard =F0=9F=93=AC fosse stato pi=C3=B9 semplice avremmo finito mo=\r\n",
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5" "lto prima."
) )
.as_bytes() .as_bytes()
); );
@@ -509,27 +472,31 @@ mod test {
#[test] #[test]
fn base64_detect() { fn base64_detect() {
let input = Body::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); let input = Body::new(vec![0; 80]);
let encoding = input.encoding(); let encoding = input.encoding();
assert_eq!(encoding, ContentTransferEncoding::Base64); assert_eq!(encoding, ContentTransferEncoding::Base64);
} }
#[test] #[test]
fn base64_encode_bytes() { fn base64_encode_bytes() {
let encoded = Body::new_with_encoding( let encoded =
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], Body::new_with_encoding(vec![0; 80], ContentTransferEncoding::Base64).unwrap();
ContentTransferEncoding::Base64,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64); assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(encoded.as_ref(), b"AAECAwQFBgcICQ=="); assert_eq!(
encoded.as_ref(),
concat!(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
)
.as_bytes()
);
} }
#[test] #[test]
fn base64_encode_bytes_wrapping() { fn base64_encode_bytes_wrapping() {
let encoded = Body::new_with_encoding( let encoded = Body::new_with_encoding(
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
ContentTransferEncoding::Base64, ContentTransferEncoding::Base64,
) )
.unwrap(); .unwrap();
@@ -578,4 +545,31 @@ mod test {
.as_bytes() .as_bytes()
); );
} }
#[test]
fn crlf() {
let mut string = String::from("Send me a ✉️\nwith\nlettre!\n😀");
in_place_crlf_line_endings(&mut string);
assert_eq!(string, "Send me a ✉️\r\nwith\r\nlettre!\r\n😀");
}
#[test]
fn harsh_crlf() {
let mut string = String::from("\n\nSend me a ✉️\r\n\nwith\n\nlettre!\n\r\n😀");
in_place_crlf_line_endings(&mut string);
assert_eq!(
string,
"\r\n\r\nSend me a ✉️\r\n\r\nwith\r\n\r\nlettre!\r\n\r\n😀"
);
}
#[test]
fn crlf_noop() {
let mut string = String::from("\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
in_place_crlf_line_endings(&mut string);
assert_eq!(string, "\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
}
} }

616
src/message/dkim.rs Normal file
View File

@@ -0,0 +1,616 @@
use std::{
borrow::Cow,
error::Error as StdError,
fmt::{self, Display},
time::SystemTime,
};
use ed25519_dalek::Signer;
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs1v15::Pkcs1v15Sign, RsaPrivateKey};
use sha2::{Digest, Sha256};
use crate::message::{
header::{HeaderName, HeaderValue},
Headers, Message,
};
/// Describe Dkim Canonicalization to apply to either body or headers
#[derive(Copy, Clone, Debug)]
pub enum DkimCanonicalizationType {
Simple,
Relaxed,
}
impl Display for DkimCanonicalizationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
DkimCanonicalizationType::Simple => "simple",
DkimCanonicalizationType::Relaxed => "relaxed",
})
}
}
/// Describe Canonicalization to be applied before signing
#[derive(Copy, Clone, Debug)]
pub struct DkimCanonicalization {
pub header: DkimCanonicalizationType,
pub body: DkimCanonicalizationType,
}
impl Default for DkimCanonicalization {
fn default() -> Self {
DkimCanonicalization {
header: DkimCanonicalizationType::Simple,
body: DkimCanonicalizationType::Relaxed,
}
}
}
/// Format canonicalization to be shown in Dkim header
impl Display for DkimCanonicalization {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.header, self.body)
}
}
/// Describe the algorithm used for signing the message
#[derive(Copy, Clone, Debug)]
pub enum DkimSigningAlgorithm {
Rsa,
Ed25519,
}
impl Display for DkimSigningAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
DkimSigningAlgorithm::Rsa => "rsa",
DkimSigningAlgorithm::Ed25519 => "ed25519",
})
}
}
/// Describe DkimSigning key error
#[derive(Debug)]
pub struct DkimSigningKeyError(InnerDkimSigningKeyError);
#[derive(Debug)]
enum InnerDkimSigningKeyError {
Base64(base64::DecodeError),
Rsa(rsa::pkcs1::Error),
Ed25519(ed25519_dalek::ed25519::Error),
}
impl Display for DkimSigningKeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match &self.0 {
InnerDkimSigningKeyError::Base64(_err) => "base64 decode error",
InnerDkimSigningKeyError::Rsa(_err) => "rsa decode error",
InnerDkimSigningKeyError::Ed25519(_err) => "ed25519 decode error",
})
}
}
impl StdError for DkimSigningKeyError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(match &self.0 {
InnerDkimSigningKeyError::Base64(err) => err,
InnerDkimSigningKeyError::Rsa(err) => err,
InnerDkimSigningKeyError::Ed25519(err) => err,
})
}
}
/// Describe a signing key to be carried by DkimConfig struct
#[derive(Debug)]
pub struct DkimSigningKey(InnerDkimSigningKey);
#[derive(Debug)]
enum InnerDkimSigningKey {
Rsa(RsaPrivateKey),
Ed25519(ed25519_dalek::SigningKey),
}
impl DkimSigningKey {
pub fn new(
private_key: &str,
algorithm: DkimSigningAlgorithm,
) -> Result<DkimSigningKey, DkimSigningKeyError> {
Ok(Self(match algorithm {
DkimSigningAlgorithm::Rsa => InnerDkimSigningKey::Rsa(
RsaPrivateKey::from_pkcs1_pem(private_key)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(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 {
match self.0 {
InnerDkimSigningKey::Rsa(_) => DkimSigningAlgorithm::Rsa,
InnerDkimSigningKey::Ed25519(_) => DkimSigningAlgorithm::Ed25519,
}
}
}
/// A struct to describe Dkim configuration applied when signing a message
#[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,
}
impl DkimConfig {
/// Create a default signature configuration with a set of headers and "simple/relaxed"
/// canonicalization
pub fn default_config(
selector: String,
domain: String,
private_key: DkimSigningKey,
) -> DkimConfig {
DkimConfig {
selector,
domain,
private_key,
headers: vec![
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("Subject"),
HeaderName::new_from_ascii_str("To"),
HeaderName::new_from_ascii_str("Date"),
],
canonicalization: DkimCanonicalization {
header: DkimCanonicalizationType::Simple,
body: DkimCanonicalizationType::Relaxed,
},
}
}
/// Create a DkimConfig
pub fn new(
selector: String,
domain: String,
private_key: DkimSigningKey,
headers: Vec<HeaderName>,
canonicalization: DkimCanonicalization,
) -> DkimConfig {
DkimConfig {
selector,
domain,
private_key,
headers,
canonicalization,
}
}
}
/// Create a Headers struct with a Dkim-Signature Header created from given parameters
fn dkim_header_format(
config: &DkimConfig,
timestamp: u64,
headers_list: &str,
body_hash: &str,
signature: &str,
) -> Headers {
let mut headers = Headers::new();
let header_name =
dkim_canonicalize_header_tag("DKIM-Signature", config.canonicalization.header);
let header_name = HeaderName::new_from_ascii(header_name.into()).unwrap();
headers.insert_raw(HeaderValue::new(header_name, format!("v=1; a={signing_algorithm}-sha256; d={domain}; s={selector}; c={canon}; q=dns/txt; t={timestamp}; h={headers_list}; bh={body_hash}; b={signature}",domain=config.domain, selector=config.selector,canon=config.canonicalization,timestamp=timestamp,headers_list=headers_list,body_hash=body_hash,signature=signature,signing_algorithm=config.private_key.get_signing_algorithm())));
headers
}
/// Canonicalize the body of an email
fn dkim_canonicalize_body(
mut body: &[u8],
canonicalization: DkimCanonicalizationType,
) -> Cow<'_, [u8]> {
match canonicalization {
DkimCanonicalizationType::Simple => {
// Remove empty lines at end
while body.ends_with(b"\r\n\r\n") {
body = &body[..body.len() - 2];
}
Cow::Borrowed(body)
}
DkimCanonicalizationType::Relaxed => {
let mut out = Vec::with_capacity(body.len());
loop {
match body {
[b' ' | b'\t', b'\r', b'\n', ..] => {}
[b' ' | b'\t', b' ' | b'\t', ..] => {}
[b' ' | b'\t', ..] => out.push(b' '),
[c, ..] => out.push(*c),
[] => break,
}
body = &body[1..];
}
// Remove empty lines at end
while out.ends_with(b"\r\n\r\n") {
out.truncate(out.len() - 2);
}
Cow::Owned(out)
}
}
}
fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
let mut r = String::with_capacity(headers.len());
fn skip_whitespace(h: &str) -> &str {
match h.as_bytes().first() {
Some(b' ' | b'\t') => skip_whitespace(&h[1..]),
_ => h,
}
}
fn name(h: &str, out: &mut String) {
if let Some(name_end) = h.bytes().position(|c| c == b':') {
let (name, rest) = h.split_at(name_end + 1);
*out += name;
// Space after header colon is stripped.
value(skip_whitespace(rest), out);
} else {
// This should never happen.
*out += h;
}
}
fn value(h: &str, out: &mut String) {
match h.as_bytes() {
// Continuation lines.
[b'\r', b'\n', b' ' | b'\t', ..] => {
out.push(' ');
value(skip_whitespace(&h[2..]), out);
}
// End of header.
[b'\r', b'\n', ..] => {
*out += "\r\n";
name(&h[2..], out)
}
// Sequential whitespace.
[b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out),
// All whitespace becomes spaces.
[b'\t', ..] => {
out.push(' ');
value(&h[1..], out)
}
[_, ..] => {
let mut chars = h.chars();
out.push(chars.next().unwrap());
value(chars.as_str(), out)
}
[] => {}
}
}
name(headers, &mut r);
r
}
/// Canonicalize header tag
fn dkim_canonicalize_header_tag(
name: &str,
canonicalization: DkimCanonicalizationType,
) -> Cow<'_, str> {
match canonicalization {
DkimCanonicalizationType::Simple => Cow::Borrowed(name),
DkimCanonicalizationType::Relaxed => Cow::Owned(name.to_lowercase()),
}
}
/// Canonicalize signed headers passed as headers_list among mail_headers using canonicalization
fn dkim_canonicalize_headers<'a>(
headers_list: impl IntoIterator<Item = &'a str>,
mail_headers: &Headers,
canonicalization: DkimCanonicalizationType,
) -> String {
let mut covered_headers = Headers::new();
for name in headers_list {
if let Some(h) = mail_headers.find_header(name) {
let name = dkim_canonicalize_header_tag(name, canonicalization);
covered_headers.insert_raw(HeaderValue::dangerous_new_pre_encoded(
HeaderName::new_from_ascii(name.into()).unwrap(),
h.get_raw().into(),
h.get_encoded().into(),
));
}
}
let serialized = covered_headers.to_string();
match canonicalization {
DkimCanonicalizationType::Simple => serialized,
DkimCanonicalizationType::Relaxed => dkim_canonicalize_headers_relaxed(&serialized),
}
}
/// 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) {
dkim_sign_fixed_time(message, dkim_config, SystemTime::now())
}
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
let timestamp = timestamp
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let headers = message.headers();
let body_hash = Sha256::digest(dkim_canonicalize_body(
&message.body_raw(),
dkim_config.canonicalization.body,
));
let bh = crate::base64::encode(body_hash);
let mut signed_headers_list =
dkim_config
.headers
.iter()
.fold(String::new(), |mut list, header| {
if !list.is_empty() {
list.push(':');
}
list.push_str(header);
list
});
if let DkimCanonicalizationType::Relaxed = dkim_config.canonicalization.header {
signed_headers_list.make_ascii_lowercase();
}
let dkim_header = dkim_header_format(dkim_config, timestamp, &signed_headers_list, &bh, "");
let signed_headers = dkim_canonicalize_headers(
dkim_config.headers.iter().map(|h| h.as_ref()),
headers,
dkim_config.canonicalization.header,
);
let canonicalized_dkim_header = dkim_canonicalize_headers(
["DKIM-Signature"],
&dkim_header,
dkim_config.canonicalization.header,
);
let mut hashed_headers = Sha256::new();
hashed_headers.update(signed_headers.as_bytes());
hashed_headers.update(canonicalized_dkim_header.trim_end().as_bytes());
let hashed_headers = hashed_headers.finalize();
let signature = match &dkim_config.private_key.0 {
InnerDkimSigningKey::Rsa(private_key) => crate::base64::encode(
private_key
.sign(Pkcs1v15Sign::new::<Sha256>(), &hashed_headers)
.unwrap(),
),
InnerDkimSigningKey::Ed25519(private_key) => {
crate::base64::encode(private_key.sign(&hashed_headers).to_bytes())
}
};
let dkim_header = dkim_header_format(
dkim_config,
timestamp,
&signed_headers_list,
&bh,
&signature,
);
message.headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("DKIM-Signature"),
dkim_header.get_raw("DKIM-Signature").unwrap().to_owned(),
));
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::{
super::{
header::{HeaderName, HeaderValue},
Header, Message,
},
dkim_canonicalize_body, dkim_canonicalize_headers, dkim_sign_fixed_time,
DkimCanonicalization, DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm,
DkimSigningKey,
};
use crate::StdError;
const KEY_RSA: &str = "-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwOsW7UFcWn1ch3UM8Mll5qZH5hVHKJQ8Z0tUlebUECq0vjw6
VcsIucZ/B70VpCN63whyi7oApdCIS1o0zad7f0UaW/BfxXADqdcFL36uMaG0RHer
uSASjQGnsl9Kozt/dXiDZX5ngjr/arLJhNZSNR4/9VSwqbE2OPXaSaQ9BsqneD0P
8dCVSfkkDZCcfC2864z7hvC01lFzWQKF36ZAoGBERHScHtFMAzUOgGuqqPiP5khw
DQB3Ffccf+BsWLU2OOteshUwTGjpoangbPCYj6kckwNm440lQwuqTinpC92yyIE5
Ol8psNMW49DLowAeZb6JrjLhD+wY9bghTaOkcwIDAQABAoIBAHTZ8LkkrdvhsvoZ
XA088AwVC9fBa6iYoT2v0zw45JomQ/Q2Zt8wa8ibAradQU56byJI65jWwS2ucd+y
c+ldWOBt6tllb50XjCCDrRBnmvtVBuux0MIBOztNlVXlgj/8+ecdZ/lB51Bqi+sF
ACsF5iVmfTcMZTVjsYQu5llUseI6Lwgqpx6ktaXD2PVsVo9Gf01ssZ4GCy69wB/3
20CsOz4LEpSYkq1oE98lMMGCfD7py3L9kWHYNNisam78GM+1ynRxRGwEDUbz6pxs
fGPIAwHLaZsOmibPkBB0PJTW742w86qQ8KAqC6ZbRYOF19rSMj3oTfRnPMHn9Uu5
N8eQcoECgYEA97SMUrz2hqII5i8igKylO9kV8pjcIWKI0rdt8MKj4FXTNYjjO9I+
41ONOjhUOpFci/G3YRKi8UiwbKxIRTvIxNMh2xj6Ws3iO9gQHK1j8xTWxJdjEBEz
EuZI59Mi5H7fxSL1W+n8nS8JVsaH93rvQErngqTUAsihAzjxHWdFwm0CgYEAx2Dh
claESJP2cOKgYp+SUNwc26qMaqnl1f37Yn+AflrQOfgQqJe5TRbicEC+nFlm6XUt
3st1Nj29H0uOMmMZDmDCO+cOs5Qv5A9pG6jSC6wM+2KNHQDtrxlakBFygePEPVVy
GXaY9DRa9Q4/4ataxDR2/VvIAWfEEtMTJIBDtl8CgYAIXEuwLziS6r0qJ8UeWrVp
A7a97XLgnZbIpfBMBAXL+JmcYPZqenos6hEGOgh9wZJCFvJ9kEd3pWBvCpGV5KKu
IgIuhvVMQ06zfmNs1F1fQwDMud9aF3qF1Mf5KyMuWynqWXe2lns0QvYpu6GzNK8G
mICf5DhTr7nfhfh9aZLtMQKBgCxKsmqzG5n//MxhHB4sstVxwJtwDNeZPKzISnM8
PfBT/lQSbqj1Y73japRjXbTgC4Ore3A2JKjTGFN+dm1tJGDUT/H8x4BPWEBCyCfT
3i2noA6sewrJbQPsDvlYVubSEYNKmxlbBmmhw98StlBMv9I8kX6BSDI/uggwid0e
/WvjAoGBAKpZ0UOKQyrl9reBiUfrpRCvIMakBMd79kNiH+5y0Soq/wCAnAuABayj
XEIBhFv+HxeLEnT7YV+Zzqp5L9kKw/EU4ik3JX/XsEihdSxEuGX00ZYOw05FEfpW
cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
-----END RSA PRIVATE KEY-----";
#[derive(Clone)]
struct TestHeader(String);
impl Header for TestHeader {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Test")
}
fn parse(s: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
Ok(Self(s.into()))
}
fn display(&self) -> HeaderValue {
HeaderValue::new(Self::name(), self.0.clone())
}
}
fn test_message() -> Message {
Message::builder()
.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_owned()))
.subject("Test with utf-8 ë")
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_owned()).unwrap()
}
#[test]
fn test_headers_simple_canonicalize() {
let message = test_message();
dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n")
}
#[test]
fn test_headers_relaxed_canonicalize() {
let message = test_message();
dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
}
#[test]
fn test_body_simple_canonicalize() {
let body = b" C \r\nD \t E\r\n\r\n\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Simple).into_owned(),
b" C \r\nD \t E\r\n"
);
}
#[test]
fn test_body_relaxed_canonicalize() {
let body = b" C \r\nD \t E\r\n\tF\r\n\t\r\n\r\n\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Relaxed).into_owned(),
b" C\r\nD E\r\n F\r\n"
);
}
#[test]
fn test_signature_rsa_simple() {
let mut message = test_message();
let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
dkim_sign_fixed_time(
&mut message,
&DkimConfig::new(
"dkimtest".to_owned(),
"example.org".to_owned(),
signing_key,
vec![
HeaderName::new_from_ascii_str("Date"),
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("Subject"),
HeaderName::new_from_ascii_str("To"),
],
DkimCanonicalization {
header: DkimCanonicalizationType::Simple,
body: DkimCanonicalizationType::Simple,
},
),
std::time::UNIX_EPOCH,
);
let signed = message.formatted();
let signed = std::str::from_utf8(&signed).unwrap();
assert_eq!(
signed,
std::concat!(
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
"To: Test2 <test2@example.org>\r\n",
"Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
"Test: test test very very long with spaces and extra spaces \twill be\r\n",
" folded to several lines \r\n",
"Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
" c=simple/simple; q=dns/txt; t=0; h=Date:From:Subject:To;\r\n",
" bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=;\r\n",
" b=NhoIMMAALoSgu5lKAR0+MUQunOWnU7wpF9ORUFtpxq9sGZDo9AX43AMhFemyM5W204jpFwMU6pm7AMR1nOYBdSYye4yUALtvT2nqbJBwSh7JeYu+z22t1RFKp7qQR1il8aSrkbZuNMFHYuSEwW76QtKwcNqP4bQOzS9CzgQp0ABu8qwYPBr/EypykPTfqjtyN+ywrfdqjjGOzTpRGolH0hc3CrAETNjjHbNBgKgucXmXTN7hMRdzqWjeFPxizXwouwNAavFClPG0l33gXVArFWn+CkgA84G/s4zuJiF7QPZR87Pu4pw/vIlSXxH4a42W3tT19v9iBTH7X7ldYegtmQ==\r\n",
"\r\n",
"test\r\n",
"\r\n",
"test \ttest\r\n",
"\r\n",
"\r\n",
)
);
}
#[test]
fn test_signature_rsa_relaxed() {
let mut message = test_message();
let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
dkim_sign_fixed_time(
&mut message,
&DkimConfig::new(
"dkimtest".to_owned(),
"example.org".to_owned(),
signing_key,
vec![
HeaderName::new_from_ascii_str("Date"),
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("Subject"),
HeaderName::new_from_ascii_str("To"),
],
DkimCanonicalization {
header: DkimCanonicalizationType::Relaxed,
body: DkimCanonicalizationType::Relaxed,
},
),
std::time::UNIX_EPOCH,
);
let signed = message.formatted();
let signed = std::str::from_utf8(&signed).unwrap();
println!("{signed}");
assert_eq!(
signed,
std::concat!(
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
"To: Test2 <test2@example.org>\r\n",
"Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
"Test: test test very very long with spaces and extra spaces \twill be\r\n",
" folded to several lines \r\n","Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
" c=relaxed/relaxed; q=dns/txt; t=0; h=date:from:subject:to;\r\n",
" bh=qN8je6qJgWFGSnN2MycC/XKPbN6BOrMJyAX2h4m19Ss=;\r\n",
" b=YaVfmH8dbGEywoLJ4uhbvYqDyQG1UGKFH3PE7zXGgk+YFxUgkwWjoA3aQupDNQtfTjfUsNe0dnrjyZP+ylnESpZBpbCIf5/n3FEh6j3RQthqNbQblcfH/U8mazTuRbVjYBbTZQDaQCMPTz+8D+ZQfXo2oq6dGzTuGvmuYft0CVsq/BIp/EkhZHqiphDeVJSHD4iKW8+L2XwEWThoY92xOYc1G0TtBwz2UJgtiHX2YulH/kRBHeK3dKn9RTNVL3VZ+9ZrnFwIhET9TPGtU2I+q0EMSWF9H9bTrASMgW/U+E0VM2btqJlrTU6rQ7wlQeHdwecLnzXcyhCUInF1+veMNw==\r\n",
"\r\n",
"test\r\n",
"\r\n",
"test \ttest\r\n",
"\r\n",
"\r\n",
)
);
}
}

View File

@@ -1,35 +1,45 @@
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{ use std::{
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult}, fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
str::{from_utf8, FromStr}, str::FromStr,
}; };
header! { use super::{Header, HeaderName, HeaderValue};
/// `Content-Id` header, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7) use crate::BoxError;
(ContentId, "Content-ID") => [String]
}
/// `Content-Transfer-Encoding` of the body /// `Content-Transfer-Encoding` of the body
/// ///
/// The `Message` builder takes care of choosing the most /// The `Message` builder takes care of choosing the most
/// efficient encoding based on the chosen body, so in most /// efficient encoding based on the chosen body, so in most
/// use-caches this header shouldn't be set manually. /// use-caches this header shouldn't be set manually.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default)]
pub enum ContentTransferEncoding { pub enum ContentTransferEncoding {
/// ASCII
SevenBit, SevenBit,
/// Quoted-Printable encoding
QuotedPrintable, QuotedPrintable,
/// base64 encoding
#[default]
Base64, Base64,
// 8BITMIME /// Requires `8BITMIME`
EightBit, EightBit,
/// Binary data
Binary, Binary,
} }
impl Default for ContentTransferEncoding { impl Header for ContentTransferEncoding {
fn default() -> Self { fn name() -> HeaderName {
ContentTransferEncoding::Base64 HeaderName::new_from_ascii_str("Content-Transfer-Encoding")
}
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(s.parse()?)
}
fn display(&self) -> HeaderValue {
let val = self.to_string();
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
} }
} }
@@ -59,35 +69,12 @@ impl FromStr for ContentTransferEncoding {
} }
} }
impl Header for ContentTransferEncoding {
fn header_name() -> &'static str {
"Content-Transfer-Encoding"
}
// FIXME HeaderError->HeaderError, same for result
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one()
.ok_or(HeaderError::Header)
.and_then(|r| from_utf8(r).map_err(|_| HeaderError::Header))
.and_then(|s| {
s.parse::<ContentTransferEncoding>()
.map_err(|_| HeaderError::Header)
})
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}", self))
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::ContentTransferEncoding; use super::ContentTransferEncoding;
use hyperx::header::Headers; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_content_transfer_encoding() { fn format_content_transfer_encoding() {
@@ -95,35 +82,35 @@ mod test {
headers.set(ContentTransferEncoding::SevenBit); headers.set(ContentTransferEncoding::SevenBit);
assert_eq!( assert_eq!(headers.to_string(), "Content-Transfer-Encoding: 7bit\r\n");
format!("{}", headers),
"Content-Transfer-Encoding: 7bit\r\n"
);
headers.set(ContentTransferEncoding::Base64); headers.set(ContentTransferEncoding::Base64);
assert_eq!( assert_eq!(headers.to_string(), "Content-Transfer-Encoding: base64\r\n");
format!("{}", headers),
"Content-Transfer-Encoding: base64\r\n"
);
} }
#[test] #[test]
fn parse_content_transfer_encoding() { fn parse_content_transfer_encoding() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set_raw("Content-Transfer-Encoding", "7bit"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"7bit".to_owned(),
));
assert_eq!( assert_eq!(
headers.get::<ContentTransferEncoding>(), headers.get::<ContentTransferEncoding>(),
Some(&ContentTransferEncoding::SevenBit) Some(ContentTransferEncoding::SevenBit)
); );
headers.set_raw("Content-Transfer-Encoding", "base64"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"base64".to_owned(),
));
assert_eq!( assert_eq!(
headers.get::<ContentTransferEncoding>(), headers.get::<ContentTransferEncoding>(),
Some(&ContentTransferEncoding::Base64) Some(ContentTransferEncoding::Base64)
); );
} }
} }

View File

@@ -0,0 +1,127 @@
use std::fmt::Write;
use email_encoding::headers::EmailWriter;
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
/// `Content-Disposition` of an attachment
///
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
#[derive(Debug, Clone, PartialEq)]
pub struct ContentDisposition(HeaderValue);
impl ContentDisposition {
/// An attachment which should be displayed inline into the message
pub fn inline() -> Self {
Self(HeaderValue::dangerous_new_pre_encoded(
Self::name(),
"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 is downloaded
pub fn inline_with_name(file_name: &str) -> Self {
Self::with_name("inline", file_name)
}
/// An attachment which is separate from the body of the message, and can be downloaded separately
pub fn attachment(file_name: &str) -> Self {
Self::with_name("attachment", file_name)
}
fn with_name(kind: &str, file_name: &str) -> Self {
let raw_value = format!("{kind}; filename=\"{file_name}\"");
let mut encoded_value = String::new();
let line_len = "Content-Disposition: ".len();
{
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
w.write_str(kind).expect("writing `kind` returned an error");
w.write_char(';').expect("writing `;` returned an error");
w.optional_breakpoint();
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
.expect("some Write implementation returned an error");
}
Self(HeaderValue::dangerous_new_pre_encoded(
Self::name(),
raw_value,
encoded_value,
))
}
}
impl Header for ContentDisposition {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Content-Disposition")
}
fn parse(s: &str) -> Result<Self, BoxError> {
match (s.split_once(';'), s) {
(_, "inline") => Ok(Self::inline()),
(Some((kind @ ("inline" | "attachment"), file_name)), _) => file_name
.split_once(" filename=\"")
.and_then(|(_, file_name)| file_name.strip_suffix('"'))
.map(|file_name| Self::with_name(kind, file_name))
.ok_or_else(|| "Unsupported ContentDisposition value".into()),
_ => Err("Unsupported ContentDisposition value".into()),
}
}
fn display(&self) -> HeaderValue {
self.0.clone()
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::ContentDisposition;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_content_disposition() {
let mut headers = Headers::new();
headers.set(ContentDisposition::inline());
assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");
headers.set(ContentDisposition::attachment("something.txt"));
assert_eq!(
format!("{headers}"),
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
);
}
#[test]
fn parse_content_disposition() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"),
"inline".to_owned(),
));
assert_eq!(
headers.get::<ContentDisposition>(),
Some(ContentDisposition::inline())
);
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"),
"attachment; filename=\"something.txt\"".to_owned(),
));
assert_eq!(
headers.get::<ContentDisposition>(),
Some(ContentDisposition::attachment("something.txt"))
);
}
}

View File

@@ -0,0 +1,193 @@
use std::{
error::Error as StdError,
fmt::{self, Display},
str::FromStr,
};
use mime::Mime;
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
/// `Content-Type` of the body
///
/// 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
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentType(Mime);
impl ContentType {
/// A `ContentType` of type `text/plain; charset=utf-8`
///
/// Indicates that the body is in utf-8 encoded plain text.
pub const TEXT_PLAIN: ContentType = Self::from_mime(mime::TEXT_PLAIN_UTF_8);
/// A `ContentType` of type `text/html; charset=utf-8`
///
/// Indicates that the body is in utf-8 encoded html.
pub const TEXT_HTML: ContentType = Self::from_mime(mime::TEXT_HTML_UTF_8);
/// Parse `s` into `ContentType`
pub fn parse(s: &str) -> Result<ContentType, ContentTypeErr> {
Ok(Self::from_mime(s.parse().map_err(ContentTypeErr)?))
}
pub(crate) const fn from_mime(mime: Mime) -> Self {
Self(mime)
}
pub(crate) fn as_ref(&self) -> &Mime {
&self.0
}
}
impl Header for ContentType {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Content-Type")
}
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(Self(s.parse()?))
}
fn display(&self) -> HeaderValue {
HeaderValue::new(Self::name(), self.0.to_string())
}
}
impl FromStr for ContentType {
type Err = ContentTypeErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(feature = "mime03")]
#[cfg_attr(docsrs, doc(cfg(feature = "mime03")))]
impl From<Mime> for ContentType {
fn from(mime: Mime) -> Self {
Self::from_mime(mime)
}
}
/// An error occurred while trying to [`ContentType::parse`].
#[derive(Debug)]
pub struct ContentTypeErr(mime::FromStrError);
impl StdError for ContentTypeErr {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.0)
}
}
impl Display for ContentTypeErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
// -- Serialization and Deserialization --
#[cfg(feature = "serde")]
mod serde {
use std::fmt;
use serde::{
de::{self, Deserialize, Deserializer, Visitor},
ser::{Serialize, Serializer},
};
use super::ContentType;
impl Serialize for ContentType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_newtype_struct("ContentType", &format!("{}", &self.0))
}
}
impl<'de> Deserialize<'de> for ContentType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ContentTypeVisitor;
impl<'de> Visitor<'de> for ContentTypeVisitor {
type Value = ContentType;
// The error message which states what the Visitor expects to
// receive
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a ContentType string like `text/plain`")
}
fn visit_str<E>(self, mime: &str) -> Result<ContentType, E>
where
E: de::Error,
{
match ContentType::parse(mime) {
Ok(content_type) => Ok(content_type),
Err(_) => Err(E::custom(format!(
"Couldn't parse the following MIME-Type: {mime}"
))),
}
}
}
deserializer.deserialize_str(ContentTypeVisitor)
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::ContentType;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_content_type() {
let mut headers = Headers::new();
headers.set(ContentType::TEXT_PLAIN);
assert_eq!(
headers.to_string(),
"Content-Type: text/plain; charset=utf-8\r\n"
);
headers.set(ContentType::TEXT_HTML);
assert_eq!(
headers.to_string(),
"Content-Type: text/html; charset=utf-8\r\n"
);
}
#[test]
fn parse_content_type() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"),
"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_owned(),
));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
}
}

135
src/message/header/date.rs Normal file
View File

@@ -0,0 +1,135 @@
use std::time::SystemTime;
use httpdate::HttpDate;
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
/// Message `Date` header
///
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Date(HttpDate);
impl Date {
/// Build a `Date` from [`SystemTime`]
pub fn new(st: SystemTime) -> Self {
Self(st.into())
}
/// Get the current date
///
/// Shortcut for `Date::new(SystemTime::now())`
pub fn now() -> Self {
Self::new(SystemTime::now())
}
}
impl Header for Date {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Date")
}
fn parse(s: &str) -> Result<Self, BoxError> {
let mut s = String::from(s);
if s.ends_with("+0000") {
// The httpdate crate expects the `Date` to end in ` GMT`, but email
// uses `+0000` to indicate UTC, so we crudely fix this issue here.
s.truncate(s.len() - "+0000".len());
s.push_str("GMT");
}
Ok(Self(s.parse::<HttpDate>()?))
}
fn display(&self) -> HeaderValue {
let mut val = self.0.to_string();
if val.ends_with(" GMT") {
// The httpdate crate always appends ` GMT` to the end of the string,
// but this is considered an obsolete date format for email
// https://tools.ietf.org/html/rfc2822#appendix-A.6.2,
// so we replace `GMT` with `+0000`
val.truncate(val.len() - "GMT".len());
val.push_str("+0000");
}
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
}
}
impl From<SystemTime> for Date {
fn from(st: SystemTime) -> Self {
Self::new(st)
}
}
impl From<Date> for SystemTime {
fn from(this: Date) -> SystemTime {
this.0.into()
}
}
#[cfg(test)]
mod test {
use std::time::{Duration, SystemTime};
use pretty_assertions::assert_eq;
use super::Date;
use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test]
fn format_date() {
let mut headers = Headers::new();
// Tue, 15 Nov 1994 08:12:31 GMT
headers.set(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
));
assert_eq!(
headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_owned()
);
// Tue, 15 Nov 1994 08:12:32 GMT
headers.set(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
));
assert_eq!(
headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:32 +0000\r\n"
);
}
#[test]
fn parse_date() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:31 +0000".to_owned(),
));
assert_eq!(
headers.get::<Date>(),
Some(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
))
);
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:32 +0000".to_owned(),
));
assert_eq!(
headers.get::<Date>(),
Some(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
))
);
}
}

View File

@@ -1,12 +1,10 @@
use crate::message::{ use email_encoding::headers::EmailWriter;
mailbox::{Mailbox, Mailboxes},
utf8_b, use super::{Header, HeaderName, HeaderValue};
use crate::{
message::mailbox::{Mailbox, Mailboxes},
BoxError,
}; };
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{fmt::Result as FmtResult, slice::Iter, str::from_utf8};
/// Header which can contains multiple mailboxes /// Header which can contains multiple mailboxes
pub trait MailboxesHeader { pub trait MailboxesHeader {
@@ -16,27 +14,42 @@ pub trait MailboxesHeader {
macro_rules! mailbox_header { macro_rules! mailbox_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => { ($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])* $(#[$doc])*
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(pub Mailbox); pub struct $type_name(Mailbox);
impl Header for $type_name { impl Header for $type_name {
fn header_name() -> &'static str { fn name() -> HeaderName {
$header_name HeaderName::new_from_ascii_str($header_name)
} }
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self> where fn parse(s: &str) -> Result<Self, BoxError> {
T: RawLike<'a>, let mailbox: Mailbox = s.parse()?;
Self: Sized { Ok(Self(mailbox))
raw.one()
.ok_or(HeaderError::Header)
.and_then(parse_mailboxes)
.and_then(|mbs| {
mbs.into_single().ok_or(HeaderError::Header)
}).map($type_name)
} }
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { fn display(&self) -> HeaderValue {
f.fmt_line(&self.0.recode_name(utf8_b::encode)) let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len();
{
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
}
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
}
}
impl std::convert::From<Mailbox> for $type_name {
#[inline]
fn from(mailbox: Mailbox) -> Self {
Self(mailbox)
}
}
impl std::convert::From<$type_name> for Mailbox {
#[inline]
fn from(this: $type_name) -> Mailbox {
this.0
} }
} }
}; };
@@ -45,8 +58,8 @@ macro_rules! mailbox_header {
macro_rules! mailboxes_header { macro_rules! mailboxes_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => { ($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])* $(#[$doc])*
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(pub Mailboxes); pub struct $type_name(pub(crate) Mailboxes);
impl MailboxesHeader for $type_name { impl MailboxesHeader for $type_name {
fn join_mailboxes(&mut self, other: Self) { fn join_mailboxes(&mut self, other: Self) {
@@ -55,23 +68,38 @@ macro_rules! mailboxes_header {
} }
impl Header for $type_name { impl Header for $type_name {
fn header_name() -> &'static str { fn name() -> HeaderName {
$header_name HeaderName::new_from_ascii_str($header_name)
} }
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name> fn parse(s: &str) -> Result<Self, BoxError> {
where let mailbox: Mailboxes = s.parse()?;
T: RawLike<'a>, Ok(Self(mailbox))
Self: Sized, }
fn display(&self) -> HeaderValue {
let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len();
{ {
raw.one() let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
.ok_or(HeaderError::Header) self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
.and_then(parse_mailboxes)
.map($type_name)
} }
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
format_mailboxes(self.0.iter(), f) }
}
impl std::convert::From<Mailboxes> for $type_name {
#[inline]
fn from(mailboxes: Mailboxes) -> Self {
Self(mailboxes)
}
}
impl std::convert::From<$type_name> for Mailboxes {
#[inline]
fn from(this: $type_name) -> Mailboxes {
this.0
} }
} }
}; };
@@ -146,26 +174,12 @@ mailboxes_header! {
(Bcc, "Bcc") (Bcc, "Bcc")
} }
fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
if let Ok(src) = from_utf8(raw) {
if let Ok(mbs) = src.parse() {
return Ok(mbs);
}
}
Err(HeaderError::Header)
}
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&Mailboxes::from(
mbs.map(|mb| mb.recode_name(utf8_b::encode))
.collect::<Vec<_>>(),
))
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::{From, Mailbox, Mailboxes}; use super::{From, Mailbox, Mailboxes};
use hyperx::header::Headers; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_single_without_name() { fn format_single_without_name() {
@@ -174,17 +188,17 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set(From(from)); headers.set(From(from));
assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n"); assert_eq!(headers.to_string(), "From: kayo@example.com\r\n");
} }
#[test] #[test]
fn format_single_with_name() { fn format_single_with_name() {
let from = Mailboxes::new().with("K. <kayo@example.com>".parse().unwrap()); let from = Mailboxes::new().with("Kayo <kayo@example.com>".parse().unwrap());
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set(From(from)); headers.set(From(from));
assert_eq!(format!("{}", headers), "From: K. <kayo@example.com>\r\n"); assert_eq!(headers.to_string(), "From: Kayo <kayo@example.com>\r\n");
} }
#[test] #[test]
@@ -197,7 +211,7 @@ mod test {
headers.set(From(from)); headers.set(From(from));
assert_eq!( assert_eq!(
format!("{}", headers), headers.to_string(),
"From: kayo@example.com, pony@domain.tld\r\n" "From: kayo@example.com, pony@domain.tld\r\n"
); );
} }
@@ -205,7 +219,7 @@ mod test {
#[test] #[test]
fn format_multi_with_name() { fn format_multi_with_name() {
let from = vec![ let from = vec![
"K. <kayo@example.com>".parse().unwrap(), "Kayo <kayo@example.com>".parse().unwrap(),
"Pony P. <pony@domain.tld>".parse().unwrap(), "Pony P. <pony@domain.tld>".parse().unwrap(),
]; ];
@@ -213,8 +227,8 @@ mod test {
headers.set(From(from.into())); headers.set(From(from.into()));
assert_eq!( assert_eq!(
format!("{}", headers), headers.to_string(),
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n" "From: Kayo <kayo@example.com>, \"Pony P.\" <pony@domain.tld>\r\n"
); );
} }
@@ -226,7 +240,7 @@ mod test {
headers.set(From(from.into())); headers.set(From(from.into()));
assert_eq!( assert_eq!(
format!("{}", headers), headers.to_string(),
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n" "From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
); );
} }
@@ -236,9 +250,12 @@ mod test {
let from = vec!["kayo@example.com".parse().unwrap()].into(); let from = vec!["kayo@example.com".parse().unwrap()].into();
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(&From(from))); assert_eq!(headers.get::<From>(), Some(From(from)));
} }
#[test] #[test]
@@ -246,9 +263,12 @@ mod test {
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into(); let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(&From(from))); assert_eq!(headers.get::<From>(), Some(From(from)));
} }
#[test] #[test]
@@ -259,9 +279,12 @@ mod test {
]; ];
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com, pony@domain.tld"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com, pony@domain.tld".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(&From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
} }
#[test] #[test]
@@ -272,18 +295,65 @@ mod test {
]; ];
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(&From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
} }
#[test] #[test]
fn parse_single_with_utf8_name() { fn parse_multi_with_name_containing_comma() {
let from: Vec<Mailbox> = vec!["Кайо <kayo@example.com>".parse().unwrap()]; let from: Vec<Mailbox> = vec![
"\"Test, test\" <1@example.com>".parse().unwrap(),
"\"Test2, test2\" <2@example.com>".parse().unwrap(),
];
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"\"Test, test\" <1@example.com>, \"Test2, test2\" <2@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(&From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[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())));
}
#[test]
fn parse_multi_with_name_containing_comma_last_broken() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"\"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>"#
);
} }
} }

View File

@@ -1,13 +1,726 @@
//! Headers widely used in email messages //! Headers widely used in email messages
use std::{
borrow::Cow,
error::Error,
fmt::{self, Display, Formatter, Write},
ops::Deref,
};
use email_encoding::headers::EmailWriter;
pub use self::{
content::*,
content_disposition::ContentDisposition,
content_type::{ContentType, ContentTypeErr},
date::Date,
mailbox::*,
special::*,
textual::*,
};
use crate::BoxError;
mod content; mod content;
mod content_disposition;
mod content_type;
mod date;
mod mailbox; mod mailbox;
mod special; mod special;
mod textual; mod textual;
pub use self::{content::*, mailbox::*, special::*, textual::*}; /// Represents an email header
///
/// Email header as defined in [RFC5322](https://datatracker.ietf.org/doc/html/rfc5322) and extensions.
pub trait Header: Clone {
fn name() -> HeaderName;
pub use hyperx::header::{ fn parse(s: &str) -> Result<Self, BoxError>;
Charset, ContentDisposition, ContentLocation, ContentType, Date, DispositionParam,
DispositionType, Header, Headers, HttpDate as EmailDate, fn display(&self) -> HeaderValue;
}
/// A set of email headers
#[derive(Debug, Clone, Default)]
pub struct Headers {
headers: Vec<HeaderValue>,
}
impl Headers {
/// Create an empty `Headers`
///
/// This function does not allocate.
#[inline]
pub const fn new() -> Self {
Self {
headers: Vec::new(),
}
}
/// Create an empty `Headers` with a pre-allocated capacity
///
/// Pre-allocates a capacity of at least `capacity`.
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
headers: Vec::with_capacity(capacity),
}
}
/// 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> {
self.get_raw(&H::name())
.and_then(|raw_value| H::parse(raw_value).ok())
}
/// Sets `Header` into `Headers`, overriding `Header` if it
/// was already present in `Headers`
pub fn set<H: Header>(&mut self, header: H) {
self.insert_raw(header.display());
}
/// Remove `Header` from `Headers`, returning it
///
/// Returns `None` if `Header` isn't in `Headers`.
pub fn remove<H: Header>(&mut self) -> Option<H> {
self.remove_raw(&H::name())
.and_then(|value| H::parse(&value.raw_value).ok())
}
/// Clears `Headers`, removing all headers from it
///
/// Any pre-allocated capacity is left untouched.
#[inline]
pub fn clear(&mut self) {
self.headers.clear();
}
/// Returns a reference to the raw value of header `name`
///
/// Returns `None` if `name` isn't present in `Headers`.
pub fn get_raw(&self, name: &str) -> Option<&str> {
self.find_header(name).map(|value| value.raw_value.as_str())
}
/// Inserts a raw header into `Headers`, overriding `value` if it
/// was already present in `Headers`.
pub fn insert_raw(&mut self, value: HeaderValue) {
match self.find_header_mut(&value.name) {
Some(current_value) => {
*current_value = value;
}
None => {
self.headers.push(value);
}
}
}
/// Remove a raw header from `Headers`, returning it
///
/// Returns `None` if `name` isn't present in `Headers`.
pub fn remove_raw(&mut self, name: &str) -> Option<HeaderValue> {
self.find_header_index(name).map(|i| self.headers.remove(i))
}
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
self.headers
.iter()
.find(|value| name.eq_ignore_ascii_case(&value.name))
}
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
self.headers
.iter_mut()
.find(|value| name.eq_ignore_ascii_case(&value.name))
}
fn find_header_index(&self, name: &str) -> Option<usize> {
self.headers
.iter()
.enumerate()
.find(|(_i, value)| name.eq_ignore_ascii_case(&value.name))
.map(|(i, _)| i)
}
}
impl Display for Headers {
/// Formats `Headers`, ready to put them into an email
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for value in &self.headers {
f.write_str(&value.name)?;
f.write_str(": ")?;
f.write_str(&value.encoded_value)?;
f.write_str("\r\n")?;
}
Ok(())
}
}
/// A possible error when converting a `HeaderName` from another type.
// comes from `http` crate
#[allow(missing_copy_implementations)]
#[derive(Clone)]
pub struct InvalidHeaderName {
_priv: (),
}
impl fmt::Debug for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InvalidHeaderName")
// skip _priv noise
.finish()
}
}
impl fmt::Display for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid header name")
}
}
impl Error for InvalidHeaderName {}
/// A valid header name
#[derive(Debug, Clone)]
pub struct HeaderName(Cow<'static, str>);
impl HeaderName {
/// Creates a new header name
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
if !ascii.is_empty()
&& ascii.len() <= 76
&& ascii.is_ascii()
&& !ascii.contains(|c| c == ':' || c == ' ')
{
Ok(Self(Cow::Owned(ascii)))
} else {
Err(InvalidHeaderName { _priv: () })
}
}
/// Creates a new header name, panics on invalid name
pub const fn new_from_ascii_str(ascii: &'static str) -> Self {
macro_rules! static_assert {
($condition:expr) => {
let _ = [()][(!($condition)) as usize];
}; };
}
static_assert!(!ascii.is_empty());
static_assert!(ascii.len() <= 76);
let bytes = ascii.as_bytes();
let mut i = 0;
while i < bytes.len() {
static_assert!(bytes[i].is_ascii());
static_assert!(bytes[i] != b' ');
static_assert!(bytes[i] != b':');
i += 1;
}
Self(Cow::Borrowed(ascii))
}
}
impl Display for HeaderName {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self)
}
}
impl Deref for HeaderName {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<[u8]> for HeaderName {
#[inline]
fn as_ref(&self) -> &[u8] {
let s: &str = self.as_ref();
s.as_bytes()
}
}
impl AsRef<str> for HeaderName {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<HeaderName> for HeaderName {
fn eq(&self, other: &HeaderName) -> bool {
let s1: &str = self.as_ref();
let s2: &str = other.as_ref();
s1 == s2
}
}
impl PartialEq<&str> for HeaderName {
fn eq(&self, other: &&str) -> bool {
let s: &str = self.as_ref();
s == *other
}
}
impl PartialEq<HeaderName> for &str {
fn eq(&self, other: &HeaderName) -> bool {
let s: &str = other.as_ref();
*self == s
}
}
/// A safe for use header value
#[derive(Debug, Clone, PartialEq)]
pub struct HeaderValue {
name: HeaderName,
raw_value: String,
encoded_value: String,
}
impl HeaderValue {
/// Construct a new `HeaderValue` and encode it
///
/// Takes the header `name` and the `raw_value` and encodes
/// it via `RFC2047` and line folds it.
///
/// [`RFC2047`]: https://datatracker.ietf.org/doc/html/rfc2047
pub fn new(name: HeaderName, raw_value: String) -> Self {
let mut encoded_value = String::with_capacity(raw_value.len());
HeaderValueEncoder::encode(&name, &raw_value, &mut encoded_value).unwrap();
Self {
name,
raw_value,
encoded_value,
}
}
/// Construct a new `HeaderValue` using a pre-encoded header value
///
/// This method is _extremely_ dangerous as it opens up
/// the encoder to header injection attacks, but is sometimes
/// acceptable for use if `encoded_value` contains only ascii
/// printable characters and is already line folded.
///
/// When in doubt, use [`HeaderValue::new`].
pub fn dangerous_new_pre_encoded(
name: HeaderName,
raw_value: String,
encoded_value: String,
) -> Self {
Self {
name,
raw_value,
encoded_value,
}
}
#[cfg(feature = "dkim")]
pub(crate) fn get_raw(&self) -> &str {
&self.raw_value
}
#[cfg(feature = "dkim")]
pub(crate) fn get_encoded(&self) -> &str {
&self.encoded_value
}
}
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
struct HeaderValueEncoder<'a> {
writer: EmailWriter<'a>,
encode_buf: String,
}
impl<'a> HeaderValueEncoder<'a> {
fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
let encoder = Self::new(name, f);
encoder.format(value.split_inclusive(' '))
}
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
let line_len = name.len() + ": ".len();
let writer = EmailWriter::new(writer, line_len, 0, false, false);
Self {
writer,
encode_buf: String::new(),
}
}
fn format(mut self, words_iter: impl Iterator<Item = &'a str>) -> fmt::Result {
for next_word in words_iter {
let allowed = allowed_str(next_word);
if allowed {
// This word only contains allowed characters
// the next word is allowed, but we may have accumulated some words to encode
self.flush_encode_buf()?;
self.writer.folding().write_str(next_word)?;
} else {
// This word contains unallowed characters
self.encode_buf.push_str(next_word);
}
}
self.flush_encode_buf()?;
Ok(())
}
fn flush_encode_buf(&mut self) -> fmt::Result {
if self.encode_buf.is_empty() {
// nothing to encode
return Ok(());
}
let prefix = self.encode_buf.trim_end_matches(' ');
email_encoding::headers::rfc2047::encode(prefix, &mut self.writer)?;
// TODO: add a better API for doing this in email-encoding
let spaces = self.encode_buf.len() - prefix.len();
for _ in 0..spaces {
self.writer.space();
}
self.encode_buf.clear();
Ok(())
}
}
fn allowed_str(s: &str) -> bool {
s.bytes().all(allowed_char)
}
const fn allowed_char(c: u8) -> bool {
c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::{HeaderName, HeaderValue, Headers, To};
use crate::message::Mailboxes;
#[test]
fn valid_headername() {
assert!(HeaderName::new_from_ascii(String::from("From")).is_ok());
}
#[test]
fn non_ascii_headername() {
assert!(HeaderName::new_from_ascii(String::from("🌎")).is_err());
}
#[test]
fn spaces_in_headername() {
assert!(HeaderName::new_from_ascii(String::from("From ")).is_err());
}
#[test]
fn colons_in_headername() {
assert!(HeaderName::new_from_ascii(String::from("From:")).is_err());
}
#[test]
fn empty_headername() {
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
}
#[test]
fn const_valid_headername() {
let _ = HeaderName::new_from_ascii_str("From");
}
#[test]
#[should_panic]
fn const_non_ascii_headername() {
let _ = HeaderName::new_from_ascii_str("🌎");
}
#[test]
#[should_panic]
fn const_spaces_in_headername() {
let _ = HeaderName::new_from_ascii_str("From ");
}
#[test]
#[should_panic]
fn const_colons_in_headername() {
let _ = HeaderName::new_from_ascii_str("From:");
}
#[test]
#[should_panic]
fn const_empty_headername() {
let _ = HeaderName::new_from_ascii_str("");
}
// names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
#[test]
fn format_ascii() {
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_owned(),
));
assert_eq!(
headers.to_string(),
"To: John Doe <example@example.com>, Jean Dupont <jean@example.com>\r\n"
);
}
#[test]
fn format_ascii_with_folding() {
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_owned(),
));
assert_eq!(
headers.to_string(),
concat!(
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith\r\n",
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand\r\n",
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
)
);
}
#[test]
fn format_ascii_with_folding_long_line() {
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_owned()
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello! This is lettre, and this\r\n",
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
" guess that's it!\r\n"
)
);
}
#[test]
fn format_ascii_with_folding_very_long_line() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_owned()
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello!\r\n",
" IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut!\r\n",
" I don't know\r\n",
)
);
}
#[test]
fn format_ascii_with_folding_giant_word() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_owned()
));
assert_eq!(
headers.to_string(),
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz\r\n",
);
}
#[test]
fn format_special() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"Seán <sean@example.com>".to_owned(),
));
assert_eq!(
headers.to_string(),
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n"
);
}
#[test]
fn format_special_emoji() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"🌎 <world@example.com>".to_owned(),
));
assert_eq!(
headers.to_string(),
"To: =?utf-8?b?8J+Mjg==?= <world@example.com>\r\n"
);
}
#[test]
fn format_special_with_folding() {
let mut headers = Headers::new();
let to = To::from(Mailboxes::from_iter([
"🌍 <world@example.com>".parse().unwrap(),
"🦆 Everywhere <ducks@example.com>".parse().unwrap(),
"Иванов Иван Иванович <ivanov@example.com>".parse().unwrap(),
"Jānis Bērziņš <janis@example.com>".parse().unwrap(),
"Seán Ó Rudaí <sean@example.com>".parse().unwrap(),
]));
headers.set(to);
assert_eq!(
headers.to_string(),
concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhiBFdmVyeXdo?=\r\n",
" =?utf-8?b?ZXJl?= <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LI=?=\r\n",
" =?utf-8?b?0LDQvSDQmNCy0LDQvdC+0LLQuNGH?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
)
);
}
#[test]
fn format_special_with_folding_raw() {
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_owned(),
));
// TODO: fix the fact that the encoder doesn't know that
// the space between the name and the address should be
// removed when wrapping.
assert_eq!(
headers.to_string(),
concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
)
);
}
#[test]
fn format_slice_on_char_boundary_bug() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_owned(),)
);
assert_eq!(
headers.to_string(),
concat!(
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+lsw==?=\r\n"
)
);
}
#[test]
fn format_bad_stuff() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Hello! \r\n This is \" bad \0. 👋".to_owned(),
));
assert_eq!(
headers.to_string(),
"Subject: Hello! =?utf-8?b?DQo=?= This is \" bad =?utf-8?b?AC4g8J+Riw==?=\r\n"
);
}
#[test]
fn format_everything() {
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_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_owned(),
)
);
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Someone <somewhere@example.com>".to_owned(),
));
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"quoted-printable".to_owned(),
));
// TODO: fix the fact that the encoder doesn't know that
// the space between the name and the address should be
// removed when wrapping.
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello! This is lettre, and this\r\n",
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
" guess that's it!\r\n",
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
"From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
)
);
}
#[test]
fn issue_653() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg".to_owned(),
));
assert_eq!(
headers.to_string(),
concat!(
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7?=\r\n",
" =?utf-8?b?Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n",
)
);
}
}

View File

@@ -1,22 +1,62 @@
use hyperx::{ use crate::{
header::{Formatter as HeaderFormatter, Header, RawLike}, message::header::{Header, HeaderName, HeaderValue},
Error as HeaderError, Result as HyperResult, BoxError,
}; };
use std::{fmt::Result as FmtResult, str::from_utf8};
#[derive(Debug, Clone, Copy, PartialEq)]
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4) /// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct MimeVersion { pub struct MimeVersion {
pub major: u8, major: u8,
pub minor: u8, minor: u8,
} }
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 }; /// MIME version 1.0
///
/// Should be used in all MIME messages.
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
impl MimeVersion { impl MimeVersion {
pub fn new(major: u8, minor: u8) -> Self { /// Build a new `MimeVersion` header
pub const fn new(major: u8, minor: u8) -> Self {
MimeVersion { major, minor } MimeVersion { major, minor }
} }
/// Get the `major` value of this `MimeVersion` header.
#[inline]
pub const fn major(self) -> u8 {
self.major
}
/// Get the `minor` value of this `MimeVersion` header.
#[inline]
pub const fn minor(self) -> u8 {
self.minor
}
}
impl Header for MimeVersion {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("MIME-Version")
}
fn parse(s: &str) -> Result<Self, BoxError> {
let mut s = s.split('.');
let major = s
.next()
.expect("The first call to next for a Split<char> always succeeds");
let minor = s
.next()
.ok_or_else(|| String::from("MIME-Version header doesn't contain '.'"))?;
let major = major.parse()?;
let minor = minor.parse()?;
Ok(MimeVersion::new(major, minor))
}
fn display(&self) -> HeaderValue {
let val = format!("{}.{}", self.major, self.minor);
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
}
} }
impl Default for MimeVersion { impl Default for MimeVersion {
@@ -25,36 +65,12 @@ impl Default for MimeVersion {
} }
} }
impl Header for MimeVersion {
fn header_name() -> &'static str {
"MIME-Version"
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one().ok_or(HeaderError::Header).and_then(|r| {
let mut s = from_utf8(r).map_err(|_| HeaderError::Header)?.split('.');
let major = s.next().ok_or(HeaderError::Header)?;
let minor = s.next().ok_or(HeaderError::Header)?;
let major = major.parse().map_err(|_| HeaderError::Header)?;
let minor = minor.parse().map_err(|_| HeaderError::Header)?;
Ok(MimeVersion::new(major, minor))
})
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}.{}", self.major, self.minor))
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::{MimeVersion, MIME_VERSION_1_0}; use super::{MimeVersion, MIME_VERSION_1_0};
use hyperx::header::Headers; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_mime_version() { fn format_mime_version() {
@@ -62,23 +78,29 @@ mod test {
headers.set(MIME_VERSION_1_0); headers.set(MIME_VERSION_1_0);
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n"); assert_eq!(headers.to_string(), "MIME-Version: 1.0\r\n");
headers.set(MimeVersion::new(0, 1)); headers.set(MimeVersion::new(0, 1));
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n"); assert_eq!(headers.to_string(), "MIME-Version: 0.1\r\n");
} }
#[test] #[test]
fn parse_mime_version() { fn parse_mime_version() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set_raw("MIME-Version", "1.0"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"),
"1.0".to_owned(),
));
assert_eq!(headers.get::<MimeVersion>(), Some(&MIME_VERSION_1_0)); assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
headers.set_raw("MIME-Version", "0.1"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"),
"0.1".to_owned(),
));
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1))); assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
} }
} }

View File

@@ -1,34 +1,37 @@
use crate::message::utf8_b; use super::{Header, HeaderName, HeaderValue};
use hyperx::{ use crate::BoxError;
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{fmt::Result as FmtResult, str::from_utf8};
macro_rules! text_header { macro_rules! text_header {
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => { ($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
#[derive(Debug, Clone, PartialEq)]
$(#[$attr])* $(#[$attr])*
pub struct $type_name(pub String); #[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(String);
impl Header for $type_name { impl Header for $type_name {
fn header_name() -> &'static str { fn name() -> HeaderName {
$header_name HeaderName::new_from_ascii_str($header_name)
} }
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name> fn parse(s: &str) -> Result<Self, BoxError> {
where Ok(Self(s.into()))
T: RawLike<'a>,
Self: Sized,
{
raw.one()
.ok_or(HeaderError::Header)
.and_then(parse_text)
.map($type_name)
} }
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult { fn display(&self) -> HeaderValue {
fmt_text(&self.0, f) HeaderValue::new(Self::name(), self.0.clone())
}
}
impl From<String> for $type_name {
#[inline]
fn from(text: String) -> Self {
Self(text)
}
}
impl AsRef<str> for $type_name {
#[inline]
fn as_ref(&self) -> &str {
&self.0
} }
} }
}; };
@@ -62,38 +65,37 @@ text_header!(
text_header!( text_header!(
/// `Message-Id` header. Contains a unique message identifier, /// `Message-Id` header. Contains a unique message identifier,
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4) /// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
Header(MessageId, "Message-Id") Header(MessageId, "Message-ID")
); );
text_header!( text_header!(
/// `User-Agent` header. Contains information about the client, /// `User-Agent` header. Contains information about the client,
/// defined in [draft-melnikov-email-user-agent-00](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00#section-3) /// defined in [draft-melnikov-email-user-agent-00](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00#section-3)
Header(UserAgent, "User-Agent") Header(UserAgent, "User-Agent")
); );
text_header! {
fn parse_text(raw: &[u8]) -> HyperResult<String> { /// `Content-Id` header,
if let Ok(src) = from_utf8(raw) { /// defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
if let Some(txt) = utf8_b::decode(src) { Header(ContentId, "Content-ID")
return Ok(txt);
} }
} text_header! {
Err(HeaderError::Header) /// `Content-Location` header,
} /// defined in [RFC2110](https://tools.ietf.org/html/rfc2110#section-4.3)
Header(ContentLocation, "Content-Location")
fn fmt_text(s: &str, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&utf8_b::encode(s))
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::Subject; use super::Subject;
use hyperx::header::Headers; use crate::message::header::{HeaderName, HeaderValue, Headers};
#[test] #[test]
fn format_ascii() { fn format_ascii() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set(Subject("Sample subject".into())); headers.set(Subject("Sample subject".into()));
assert_eq!(format!("{}", headers), "Subject: Sample subject\r\n"); assert_eq!(headers.to_string(), "Subject: Sample subject\r\n");
} }
#[test] #[test]
@@ -102,33 +104,33 @@ mod test {
headers.set(Subject("Тема сообщения".into())); headers.set(Subject("Тема сообщения".into()));
assert_eq!( assert_eq!(
format!("{}", headers), headers.to_string(),
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n" "Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
); );
} }
#[test]
fn format_utf8_word() {
let mut headers = Headers::new();
headers.set(Subject("Administratör".into()));
assert_eq!(
headers.to_string(),
"Subject: =?utf-8?b?QWRtaW5pc3RyYXTDtnI=?=\r\n"
);
}
#[test] #[test]
fn parse_ascii() { fn parse_ascii() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set_raw("Subject", "Sample subject"); headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"),
"Sample subject".to_owned(),
));
assert_eq!( assert_eq!(
headers.get::<Subject>(), headers.get::<Subject>(),
Some(&Subject("Sample subject".into())) Some(Subject("Sample subject".into()))
);
}
#[test]
fn parse_utf8() {
let mut headers = Headers::new();
headers.set_raw(
"Subject",
"=?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=",
);
assert_eq!(
headers.get::<Subject>(),
Some(&Subject("Тема сообщения".into()))
); );
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,19 @@
use crate::message::{Mailbox, Mailboxes}; use std::fmt::{Formatter, Result as FmtResult};
use serde::{ use serde::{
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor}, de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
ser::Serializer, ser::Serializer,
Deserialize, Serialize, Deserialize, Serialize,
}; };
use std::fmt::{Formatter, Result as FmtResult};
use crate::message::{Mailbox, Mailboxes};
impl Serialize for Mailbox { impl Serialize for Mailbox {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(&self.to_string()) serializer.collect_str(self)
} }
} }
@@ -109,7 +111,7 @@ impl Serialize for Mailboxes {
where where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(&self.to_string()) serializer.collect_str(self)
} }
} }
@@ -152,9 +154,11 @@ impl<'de> Deserialize<'de> for Mailboxes {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use serde_json::from_str;
use super::*; use super::*;
use crate::address::Address; use crate::address::Address;
use serde_json::from_str;
#[test] #[test]
fn parse_address_string() { fn parse_address_string() {
@@ -175,7 +179,7 @@ mod test {
} }
#[test] #[test]
fn parse_mailbox_object_address_stirng() { fn parse_mailbox_object_address_string() {
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap(); let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap()); assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
} }
@@ -194,7 +198,7 @@ mod test {
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap(); from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
assert_eq!( assert_eq!(
m, m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>" "yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse() .parse()
.unwrap() .unwrap()
); );
@@ -207,7 +211,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
m, m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>" "yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse() .parse()
.unwrap() .unwrap()
); );

View File

@@ -1,19 +1,21 @@
use crate::{
address::{Address, AddressError},
message::utf8_b,
};
use std::{ use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult, Write}, fmt::{Display, Formatter, Result as FmtResult, Write},
mem,
slice::Iter, slice::Iter,
str::FromStr, 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. /// Represents an email address with an optional name for the sender/recipient.
/// ///
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_). /// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
/// ///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/). /// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
/// ///
/// # Examples /// # Examples
/// ///
@@ -67,21 +69,29 @@ impl Mailbox {
Mailbox { name, email } Mailbox { name, email }
} }
/// Encode addressee name using function pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
pub(crate) fn recode_name<F>(&self, f: F) -> Self if let Some(name) = &self.name {
where email_encoding::headers::quoted_string::encode(name, w)?;
F: FnOnce(&str) -> String, w.optional_breakpoint();
{ w.write_char('<')?;
Mailbox::new(self.name.clone().map(|s| f(&s)), self.email.clone()) }
w.write_str(self.email.as_ref())?;
if self.name.is_some() {
w.write_char('>')?;
}
Ok(())
} }
} }
impl Display for Mailbox { impl Display for Mailbox {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(ref name) = self.name { if let Some(name) = &self.name {
let name = name.trim(); let name = name.trim();
if !name.is_empty() { if !name.is_empty() {
f.write_str(&name)?; write_word(f, name)?;
f.write_str(" <")?; f.write_str(" <")?;
self.email.fmt(f)?; self.email.fmt(f)?;
return f.write_char('>'); return f.write_char('>');
@@ -100,40 +110,24 @@ impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
} }
} }
/*
impl<S: AsRef<&str>, T: AsRef<&str>> TryFrom<(S, T)> for Mailbox {
type Error = AddressError;
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
let (name, address) = header;
Ok(Mailbox::new(Some(name.as_ref()), address.as_ref().parse()?))
}
}*/
impl FromStr for Mailbox { impl FromStr for Mailbox {
type Err = AddressError; type Err = AddressError;
fn from_str(src: &str) -> Result<Mailbox, Self::Err> { fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
match (src.find('<'), src.find('>')) { let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => { // TODO: improve error management
let name = src.split_at(addr_open).0; AddressError::InvalidInput
let addr_open = addr_open + 1; })?;
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
let addr = addr.parse()?; let mailbox = Mailbox::new(name, Address::new(user, domain)?);
let name = name.trim();
let name = if name.is_empty() { Ok(mailbox)
None
} else {
Some(name.into())
};
Ok(Mailbox::new(name, addr))
}
(Some(_), _) => Err(AddressError::Unbalanced),
_ => {
let addr = src.parse()?;
Ok(Mailbox::new(None, addr))
} }
} }
impl From<Address> for Mailbox {
fn from(value: Address) -> Self {
Self::new(None, value)
} }
} }
@@ -141,7 +135,7 @@ impl FromStr for Mailbox {
/// ///
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._). /// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
/// ///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/). /// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailboxes(Vec<Mailbox>); pub struct Mailboxes(Vec<Mailbox>);
@@ -261,6 +255,20 @@ impl Mailboxes {
pub fn iter(&self) -> Iter<'_, Mailbox> { pub fn iter(&self) -> Iter<'_, Mailbox> {
self.0.iter() self.0.iter()
} }
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
let mut first = true;
for mailbox in self.iter() {
if !mem::take(&mut first) {
w.write_char(',')?;
w.optional_breakpoint();
}
mailbox.encode(w)?;
}
Ok(())
}
} }
impl Default for Mailboxes { impl Default for Mailboxes {
@@ -293,6 +301,18 @@ impl From<Mailboxes> for Vec<Mailbox> {
} }
} }
impl FromIterator<Mailbox> for Mailboxes {
fn from_iter<T: IntoIterator<Item = Mailbox>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
self.0.extend(iter);
}
}
impl IntoIterator for Mailboxes { impl IntoIterator for Mailboxes {
type Item = Mailbox; type Item = Mailbox;
type IntoIter = ::std::vec::IntoIter<Mailbox>; type IntoIter = ::std::vec::IntoIter<Mailbox>;
@@ -302,14 +322,6 @@ impl IntoIterator for Mailboxes {
} }
} }
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
for elem in iter {
self.0.push(elem);
}
}
}
impl Display for Mailboxes { impl Display for Mailboxes {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut iter = self.iter(); let mut iter = self.iter();
@@ -331,30 +343,113 @@ impl FromStr for Mailboxes {
type Err = AddressError; type Err = AddressError;
fn from_str(src: &str) -> Result<Self, Self::Err> { fn from_str(src: &str) -> Result<Self, Self::Err> {
src.split(',') let mut mailboxes = Vec::new();
.map(|m| {
m.trim().parse().and_then(|Mailbox { name, email }| { let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
if let Some(name) = name { // TODO: improve error management
if let Some(name) = utf8_b::decode(&name) { AddressError::InvalidInput
Ok(Mailbox::new(Some(name), email)) })?;
} else {
Err(AddressError::InvalidUtf8b) for (name, (user, domain)) in parsed_mailboxes {
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
} }
} else {
Ok(Mailbox::new(None, email)) Ok(Mailboxes(mailboxes))
}
}
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
fn write_word(f: &mut Formatter<'_>, s: &str) -> FmtResult {
if s.as_bytes().iter().copied().all(is_valid_atom_char) {
f.write_str(s)
} else {
// Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
f.write_char('"')?;
for c in s.chars() {
write_quoted_string_char(f, c)?;
}
f.write_char('"')?;
Ok(())
}
}
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4
fn is_valid_atom_char(c: u8) -> bool {
matches!(c,
// Not really allowed but can be inserted between atoms.
b'\t' |
b' ' |
b'!' |
b'#' |
b'$' |
b'%' |
b'&' |
b'\'' |
b'*' |
b'+' |
b'-' |
b'/' |
b'0'..=b'8' |
b'=' |
b'?' |
b'A'..=b'Z' |
b'^' |
b'_' |
b'`' |
b'a'..=b'z' |
b'{' |
b'|' |
b'}' |
b'~' |
// Not technically allowed but will be escaped into allowed characters.
128..=255)
}
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
match c {
// Can not be encoded.
'\n' | '\r' => Err(std::fmt::Error),
// Note, not qcontent but can be put before or after any qcontent.
'\t' | ' ' => f.write_char(c),
c if match c as u32 {
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
1..=8 | 11 | 12 | 14..=31 | 127 |
// The rest of the US-ASCII except \ and "
33 |
35..=91 |
93..=126 |
// Non-ascii characters will be escaped separately later.
128.. => true,
_ => false,
} =>
{
f.write_char(c)
}
_ => {
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
f.write_char('\\')?;
f.write_char(c)
} }
})
})
.collect::<Result<Vec<_>, _>>()
.map(Mailboxes)
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::Mailbox;
use std::convert::TryInto; use std::convert::TryInto;
use pretty_assertions::assert_eq;
use super::Mailbox;
#[test] #[test]
fn mailbox_format_address_only() { fn mailbox_format_address_only() {
assert_eq!( assert_eq!(
@@ -373,7 +468,63 @@ mod test {
"{}", "{}",
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap()) Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
), ),
"K. <kayo@example.com>" "\"K.\" <kayo@example.com>"
);
}
#[test]
fn mailbox_format_address_with_comma() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("Last, First".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Last, First" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_comma_and_non_ascii() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("Laşt, First".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Laşt, First" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_comma_and_quoted_non_ascii() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some(r#"Laşt, "First""#.into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Laşt, \"First\"" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_color() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("Chris's Wiki :: blog".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Chris's Wiki :: blog" <kayo@example.com>"#
); );
} }
@@ -395,7 +546,7 @@ mod test {
"{}", "{}",
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap()) Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
), ),
"K. <kayo@example.com>" "\"K.\" <kayo@example.com>"
); );
} }
@@ -437,7 +588,7 @@ mod test {
#[test] #[test]
fn parse_address_from_tuple() { fn parse_address_from_tuple() {
assert_eq!( assert_eq!(
("K.".to_string(), "kayo@example.com".to_string()).try_into(), ("K.".to_owned(), "kayo@example.com".to_owned()).try_into(),
Ok(Mailbox::new( Ok(Mailbox::new(
Some("K.".into()), Some("K.".into()),
"kayo@example.com".parse().unwrap() "kayo@example.com".parse().unwrap()

View File

@@ -1,13 +1,15 @@
use std::{io::Write, iter::repeat_with};
use mime::Mime;
use crate::message::{ use crate::message::{
header::{ContentTransferEncoding, ContentType, Header, Headers}, header::{self, ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat, IntoBody, EmailFormat, IntoBody,
}; };
use mime::Mime;
use rand::Rng;
/// MIME part variants /// MIME part variants
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Part { pub(super) enum Part {
/// Single part with content /// Single part with content
Single(SinglePart), Single(SinglePart),
@@ -15,6 +17,16 @@ pub enum Part {
Multi(MultiPart), 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 { impl EmailFormat for Part {
fn format(&self, out: &mut Vec<u8>) { fn format(&self, out: &mut Vec<u8>) {
match self { match self {
@@ -24,18 +36,6 @@ impl EmailFormat for Part {
} }
} }
impl Part {
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
/// Parts of multipart body
pub type Parts = Vec<Part>;
/// Creates builder for single part /// Creates builder for single part
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SinglePartBuilder { pub struct SinglePartBuilder {
@@ -64,7 +64,7 @@ impl SinglePartBuilder {
/// Build singlepart using body /// Build singlepart using body
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart { pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied(); let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
let body = body.into_body(maybe_encoding); let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding()); self.headers.set(body.encoding());
@@ -92,7 +92,7 @@ impl Default for SinglePartBuilder {
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// let part = SinglePart::builder() /// let part = SinglePart::builder()
/// .header(header::ContentType("text/plain; charset=utf8".parse()?)) /// .header(header::ContentType::TEXT_PLAIN)
/// .body(String::from("Текст письма в уникоде")); /// .body(String::from("Текст письма в уникоде"));
/// # Ok(()) /// # Ok(())
/// # } /// # }
@@ -110,6 +110,20 @@ impl SinglePart {
SinglePartBuilder::new() SinglePartBuilder::new()
} }
/// 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 a UTF-8 HTML content
pub fn html<T: IntoBody>(body: T) -> Self {
Self::builder()
.header(header::ContentType::TEXT_HTML)
.body(body)
}
/// Get the headers from singlepart /// Get the headers from singlepart
#[inline] #[inline]
pub fn headers(&self) -> &Headers { pub fn headers(&self) -> &Headers {
@@ -128,14 +142,20 @@ impl SinglePart {
self.format(&mut out); self.format(&mut out);
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 { impl EmailFormat for SinglePart {
fn format(&self, out: &mut Vec<u8>) { fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes()); write!(out, "{}", self.headers)
out.extend_from_slice(b"\r\n"); .expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(&self.body);
out.extend_from_slice(b"\r\n"); out.extend_from_slice(b"\r\n");
self.format_body(out);
} }
} }
@@ -144,17 +164,17 @@ impl EmailFormat for SinglePart {
pub enum MultiPartKind { pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts /// Mixed kind to combine unrelated content parts
/// ///
/// For example this kind can be used to mix email message and attachments. /// For example, this kind can be used to mix an email message and attachments.
Mixed, Mixed,
/// Alternative kind to join several variants of same email contents. /// Alternative kind to join several variants of same email contents.
/// ///
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message. /// That kind is recommended to use for joining plain (text) and rich (HTML) messages into a single email message.
Alternative, Alternative,
/// Related kind to mix content and related resources. /// Related kind to mix content and related resources.
/// ///
/// For example, you can include images into HTML content using that. /// For example, you can include images in HTML content using that.
Related, Related,
/// Encrypted kind for encrypted messages /// Encrypted kind for encrypted messages
@@ -165,33 +185,29 @@ pub enum MultiPartKind {
} }
/// Create a random MIME boundary. /// Create a random MIME boundary.
/// (Not cryptographically random)
fn make_boundary() -> String { fn make_boundary() -> String {
rand::thread_rng() repeat_with(fastrand::alphanumeric).take(40).collect()
.sample_iter(rand::distributions::Alphanumeric)
.take(40)
.map(char::from)
.collect()
} }
impl MultiPartKind { impl MultiPartKind {
fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime { pub(crate) fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
let boundary = boundary.map_or_else(make_boundary, |s| s.into()); let boundary = boundary.map_or_else(make_boundary, Into::into);
use self::MultiPartKind::*;
format!( format!(
"multipart/{}; boundary=\"{}\"{}", "multipart/{}; boundary=\"{}\"{}",
match self { match self {
Mixed => "mixed", Self::Mixed => "mixed",
Alternative => "alternative", Self::Alternative => "alternative",
Related => "related", Self::Related => "related",
Encrypted { .. } => "encrypted", Self::Encrypted { .. } => "encrypted",
Signed { .. } => "signed", Self::Signed { .. } => "signed",
}, },
boundary, boundary,
match self { match self {
Encrypted { protocol } => format!("; protocol=\"{}\"", protocol), Self::Encrypted { protocol } => format!("; protocol=\"{protocol}\""),
Signed { protocol, micalg } => Self::Signed { protocol, micalg } =>
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg), format!("; protocol=\"{protocol}\"; micalg=\"{micalg}\""),
_ => String::new(), _ => String::new(),
} }
) )
@@ -200,18 +216,17 @@ impl MultiPartKind {
} }
fn from_mime(m: &Mime) -> Option<Self> { fn from_mime(m: &Mime) -> Option<Self> {
use self::MultiPartKind::*;
match m.subtype().as_ref() { match m.subtype().as_ref() {
"mixed" => Some(Mixed), "mixed" => Some(Self::Mixed),
"alternative" => Some(Alternative), "alternative" => Some(Self::Alternative),
"related" => Some(Related), "related" => Some(Self::Related),
"signed" => m.get_param("protocol").and_then(|p| { "signed" => m.get_param("protocol").and_then(|p| {
m.get_param("micalg").map(|micalg| Signed { m.get_param("micalg").map(|micalg| Self::Signed {
protocol: p.as_str().to_owned(), protocol: p.as_str().to_owned(),
micalg: micalg.as_str().to_owned(), micalg: micalg.as_str().to_owned(),
}) })
}), }),
"encrypted" => m.get_param("protocol").map(|p| Encrypted { "encrypted" => m.get_param("protocol").map(|p| Self::Encrypted {
protocol: p.as_str().to_owned(), protocol: p.as_str().to_owned(),
}), }),
_ => None, _ => None,
@@ -219,12 +234,6 @@ impl MultiPartKind {
} }
} }
impl From<MultiPartKind> for Mime {
fn from(m: MultiPartKind) -> Self {
m.to_mime::<String>(None)
}
}
/// Multipart builder /// Multipart builder
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MultiPartBuilder { pub struct MultiPartBuilder {
@@ -247,17 +256,17 @@ impl MultiPartBuilder {
/// Set `Content-Type` header using [`MultiPartKind`] /// Set `Content-Type` header using [`MultiPartKind`]
pub fn kind(self, kind: MultiPartKind) -> Self { pub fn kind(self, kind: MultiPartKind) -> Self {
self.header(ContentType(kind.into())) self.header(ContentType::from_mime(kind.to_mime::<String>(None)))
} }
/// Set custom boundary /// Set custom boundary
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self { pub fn boundary<S: Into<String>>(self, boundary: S) -> Self {
let kind = { let kind = {
let mime = &self.headers.get::<ContentType>().unwrap().0; let content_type = self.headers.get::<ContentType>().unwrap();
MultiPartKind::from_mime(mime).unwrap() MultiPartKind::from_mime(content_type.as_ref()).unwrap()
}; };
let mime = kind.to_mime(Some(boundary.as_ref())); let mime = kind.to_mime(Some(boundary));
self.header(ContentType(mime)) self.header(ContentType::from_mime(mime))
} }
/// Creates multipart without parts /// Creates multipart without parts
@@ -268,11 +277,6 @@ impl MultiPartBuilder {
} }
} }
/// Creates multipart using part
pub fn part(self, part: Part) -> MultiPart {
self.build().part(part)
}
/// Creates multipart using singlepart /// Creates multipart using singlepart
pub fn singlepart(self, part: SinglePart) -> MultiPart { pub fn singlepart(self, part: SinglePart) -> MultiPart {
self.build().singlepart(part) self.build().singlepart(part)
@@ -294,7 +298,7 @@ impl Default for MultiPartBuilder {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MultiPart { pub struct MultiPart {
headers: Headers, headers: Headers,
parts: Parts, parts: Vec<Part>,
} }
impl MultiPart { impl MultiPart {
@@ -338,10 +342,11 @@ impl MultiPart {
MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg }) MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
} }
/// Add part to multipart /// Alias for HTML and plain text versions of an email
pub fn part(mut self, part: Part) -> Self { pub fn alternative_plain_html<T: IntoBody, V: IntoBody>(plain: T, html: V) -> Self {
self.parts.push(part); Self::alternative()
self .singlepart(SinglePart::plain(plain))
.singlepart(SinglePart::html(html))
} }
/// Add single part to multipart /// Add single part to multipart
@@ -358,8 +363,13 @@ impl MultiPart {
/// Get the boundary of multipart contents /// Get the boundary of multipart contents
pub fn boundary(&self) -> String { pub fn boundary(&self) -> String {
let content_type = &self.headers.get::<ContentType>().unwrap().0; let content_type = self.headers.get::<ContentType>().unwrap();
content_type.get_param("boundary").unwrap().as_str().into() content_type
.as_ref()
.get_param("boundary")
.unwrap()
.as_str()
.into()
} }
/// Get the headers from the multipart /// Get the headers from the multipart
@@ -372,29 +382,15 @@ impl MultiPart {
&mut self.headers &mut self.headers
} }
/// Get the parts from the multipart
pub fn parts(&self) -> &Parts {
&self.parts
}
/// Get a mutable reference to the parts
pub fn parts_mut(&mut self) -> &mut Parts {
&mut self.parts
}
/// Get message content formatted for SMTP /// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> { pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new(); let mut out = Vec::new();
self.format(&mut out); self.format(&mut out);
out out
} }
}
impl EmailFormat for MultiPart {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
out.extend_from_slice(b"\r\n");
/// Format only the multipart body
fn format_body(&self, out: &mut Vec<u8>) {
let boundary = self.boundary(); let boundary = self.boundary();
for part in &self.parts { for part in &self.parts {
@@ -410,24 +406,33 @@ 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)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::*; use super::*;
use crate::message::header; use crate::message::header;
#[test] #[test]
fn single_part_binary() { fn single_part_binary() {
let part = SinglePart::builder() let part = SinglePart::builder()
.header(header::ContentType( .header(header::ContentType::TEXT_PLAIN)
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Binary) .header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")); .body(String::from("Текст письма в уникоде"));
assert_eq!( assert_eq!(
String::from_utf8(part.formatted()).unwrap(), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n", "Content-Transfer-Encoding: binary\r\n",
"\r\n", "\r\n",
"Текст письма в уникоде\r\n" "Текст письма в уникоде\r\n"
@@ -438,16 +443,14 @@ mod test {
#[test] #[test]
fn single_part_quoted_printable() { fn single_part_quoted_printable() {
let part = SinglePart::builder() let part = SinglePart::builder()
.header(header::ContentType( .header(header::ContentType::TEXT_PLAIN)
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::QuotedPrintable) .header(header::ContentTransferEncoding::QuotedPrintable)
.body(String::from("Текст письма в уникоде")); .body(String::from("Текст письма в уникоде"));
assert_eq!( assert_eq!(
String::from_utf8(part.formatted()).unwrap(), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n", "Content-Transfer-Encoding: quoted-printable\r\n",
"\r\n", "\r\n",
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n", "=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
@@ -459,16 +462,14 @@ mod test {
#[test] #[test]
fn single_part_base64() { fn single_part_base64() {
let part = SinglePart::builder() let part = SinglePart::builder()
.header(header::ContentType( .header(header::ContentType::TEXT_PLAIN)
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Base64) .header(header::ContentTransferEncoding::Base64)
.body(String::from("Текст письма в уникоде")); .body(String::from("Текст письма в уникоде"));
assert_eq!( assert_eq!(
String::from_utf8(part.formatted()).unwrap(), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: base64\r\n", "Content-Transfer-Encoding: base64\r\n",
"\r\n", "\r\n",
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n" "0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
@@ -479,75 +480,60 @@ mod test {
#[test] #[test]
fn multi_part_mixed() { fn multi_part_mixed() {
let part = MultiPart::mixed() let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")),
))
.singlepart( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(header::ContentType( .header(header::ContentType::TEXT_PLAIN)
"text/plain; charset=utf8".parse().unwrap(), .header(header::ContentTransferEncoding::Binary)
)) .body(String::from("Текст письма в уникоде")),
.header(header::ContentDisposition { )
disposition: header::DispositionType::Attachment, .singlepart(
parameters: vec![header::DispositionParam::Filename( SinglePart::builder()
header::Charset::Ext("utf-8".into()), .header(header::ContentType::TEXT_PLAIN)
None, .header(header::ContentDisposition::attachment("example.c"))
"example.c".into(),
)],
})
.header(header::ContentTransferEncoding::Binary) .header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")), .body(String::from("int main() { return 0; }")),
); );
assert_eq!(String::from_utf8(part.formatted()).unwrap(), assert_eq!(
concat!("Content-Type: multipart/mixed;", String::from_utf8(part.formatted()).unwrap(),
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n", concat!(
"Content-Type: multipart/mixed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n", "Content-Transfer-Encoding: binary\r\n",
"\r\n", "\r\n",
"Текст письма в уникоде\r\n", "Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf-8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n", "Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n", "Content-Transfer-Encoding: binary\r\n",
"\r\n", "\r\n",
"int main() { return 0; }\r\n", "int main() { return 0; }\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
)
);
} }
#[test] #[test]
fn multi_part_encrypted() { fn multi_part_encrypted() {
let part = MultiPart::encrypted("application/pgp-encrypted".to_owned()) let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"application/pgp-encrypted".parse().unwrap(),
))
.body(String::from("Version: 1")),
))
.singlepart( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(ContentType( .header(header::ContentType::parse("application/pgp-encrypted").unwrap())
"application/octet-stream; name=\"encrypted.asc\"" .body(String::from("Version: 1")),
.parse() )
.singlepart(
SinglePart::builder()
.header(
ContentType::parse("application/octet-stream; name=\"encrypted.asc\"")
.unwrap(), .unwrap(),
)
.header(header::ContentDisposition::inline_with_name(
"encrypted.asc",
)) ))
.header(header::ContentDisposition {
disposition: header::DispositionType::Inline,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"encrypted.asc".into(),
)],
})
.body(String::from(concat!( .body(String::from(concat!(
"-----BEGIN PGP MESSAGE-----\r\n", "-----BEGIN PGP MESSAGE-----\r\n",
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n", "wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
@@ -556,17 +542,19 @@ mod test {
))), ))),
); );
assert_eq!(String::from_utf8(part.formatted()).unwrap(), assert_eq!(
concat!("Content-Type: multipart/encrypted;", String::from_utf8(part.formatted()).unwrap(),
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";", concat!(
"Content-Type: multipart/encrypted;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
" protocol=\"application/pgp-encrypted\"\r\n", " protocol=\"application/pgp-encrypted\"\r\n",
"\r\n", "\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: application/pgp-encrypted\r\n", "Content-Type: application/pgp-encrypted\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Version: 1\r\n", "Version: 1\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n", "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n", "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
@@ -576,7 +564,9 @@ mod test {
"...\r\n", "...\r\n",
"-----END PGP MESSAGE-----\r\n", "-----END PGP MESSAGE-----\r\n",
"\r\n", "\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
)
);
} }
#[test] #[test]
fn multi_part_signed() { fn multi_part_signed() {
@@ -584,27 +574,19 @@ mod test {
"application/pgp-signature".to_owned(), "application/pgp-signature".to_owned(),
"pgp-sha256".to_owned(), "pgp-sha256".to_owned(),
) )
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType("text/plain".parse().unwrap()))
.body(String::from("Test email for signature")),
))
.singlepart( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(ContentType( .header(header::ContentType::TEXT_PLAIN)
"application/pgp-signature; name=\"signature.asc\"" .body(String::from("Test email for signature")),
.parse() )
.singlepart(
SinglePart::builder()
.header(
ContentType::parse("application/pgp-signature; name=\"signature.asc\"")
.unwrap(), .unwrap(),
)) )
.header(header::ContentDisposition { .header(header::ContentDisposition::attachment("signature.asc"))
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"signature.asc".into(),
)],
})
.body(String::from(concat!( .body(String::from(concat!(
"-----BEGIN PGP SIGNATURE-----\r\n", "-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n", "\r\n",
@@ -616,18 +598,20 @@ mod test {
))), ))),
); );
assert_eq!(String::from_utf8(part.formatted()).unwrap(), assert_eq!(
concat!("Content-Type: multipart/signed;", String::from_utf8(part.formatted()).unwrap(),
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";", concat!(
"Content-Type: multipart/signed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
" protocol=\"application/pgp-signature\";", " protocol=\"application/pgp-signature\";",
" micalg=\"pgp-sha256\"\r\n", " micalg=\"pgp-sha256\"\r\n",
"\r\n", "\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain\r\n", "Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Test email for signature\r\n", "Test email for signature\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n", "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
"Content-Disposition: attachment; filename=\"signature.asc\"\r\n", "Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
@@ -640,77 +624,76 @@ mod test {
"=3FYZ\r\n", "=3FYZ\r\n",
"-----END PGP SIGNATURE-----\r\n", "-----END PGP SIGNATURE-----\r\n",
"\r\n", "\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
)
);
} }
#[test] #[test]
fn multi_part_alternative() { fn multi_part_alternative() {
let part = MultiPart::alternative() let part = MultiPart::alternative()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.part(Part::Single(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"))))
.singlepart(SinglePart::builder() .singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap())) .header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")))
.singlepart(SinglePart::builder()
.header(header::ContentType::TEXT_HTML)
.header(header::ContentTransferEncoding::Binary) .header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>"))); .body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(), assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/alternative;", concat!("Content-Type: multipart/alternative;\r\n",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n", "Content-Transfer-Encoding: binary\r\n",
"\r\n", "\r\n",
"Текст письма в уникоде\r\n", "Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/html; charset=utf8\r\n", "Content-Type: text/html; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n", "Content-Transfer-Encoding: binary\r\n",
"\r\n", "\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n", "<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
} }
#[test] #[test]
fn multi_part_mixed_related() { fn multi_part_mixed_related() {
let part = MultiPart::mixed() let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.multipart(MultiPart::related() .multipart(MultiPart::related()
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh") .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(SinglePart::builder() .singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap())) .header(header::ContentType::TEXT_HTML)
.header(header::ContentTransferEncoding::Binary) .header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>"))) .body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
.singlepart(SinglePart::builder() .singlepart(SinglePart::builder()
.header(header::ContentType("image/png".parse().unwrap())) .header(header::ContentType::parse("image/png").unwrap())
.header(header::ContentLocation("/image.png".into())) .header(header::ContentLocation::from(String::from("/image.png")))
.header(header::ContentTransferEncoding::Base64) .header(header::ContentTransferEncoding::Base64)
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")))) .body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
.singlepart(SinglePart::builder() .singlepart(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap())) .header(header::ContentType::TEXT_PLAIN)
.header(header::ContentDisposition { .header(header::ContentDisposition::attachment("example.c"))
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".into())]
})
.header(header::ContentTransferEncoding::Binary) .header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }"))); .body(String::from("int main() { return 0; }")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(), assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed;", concat!("Content-Type: multipart/mixed;\r\n",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: multipart/related;", "Content-Type: multipart/related;\r\n",
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/html; charset=utf8\r\n", "Content-Type: text/html; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n", "Content-Transfer-Encoding: binary\r\n",
"\r\n", "\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n", "<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: image/png\r\n", "Content-Type: image/png\r\n",
"Content-Location: /image.png\r\n", "Content-Location: /image.png\r\n",
"Content-Transfer-Encoding: base64\r\n", "Content-Transfer-Encoding: base64\r\n",
@@ -718,14 +701,14 @@ mod test {
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n", "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n", "ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n", "NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf8\r\n", "Content-Type: text/plain; charset=utf-8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n", "Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n", "Content-Transfer-Encoding: binary\r\n",
"\r\n", "\r\n",
"int main() { return 0; }\r\n", "int main() { return 0; }\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
} }
#[test] #[test]

View File

@@ -4,15 +4,8 @@
//! //!
//! This section demonstrates how to build messages. //! This section demonstrates how to build messages.
//! //!
//! <!--
//! style for <details><summary>Blablabla</summary> Lots of stuff</details>
//! borrowed from https://docs.rs/time/0.2.23/src/time/lib.rs.html#49-54
//! -->
//! <style> //! <style>
//! summary, details:not([open]) { cursor: pointer; } //! summary, details:not([open]) { cursor: pointer; }
//! summary { display: list-item; }
//! summary::marker { content: '▶ '; }
//! details[open] summary::marker { content: '▼ '; }
//! </style> //! </style>
//! //!
//! //!
@@ -21,7 +14,7 @@
//! The easiest way of creating a message, which uses a plain text body. //! The easiest way of creating a message, which uses a plain text body.
//! //!
//! ```rust //! ```rust
//! use lettre::message::Message; //! use lettre::message::{header::ContentType, Message};
//! //!
//! # use std::error::Error; //! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
@@ -30,6 +23,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?) //! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year") //! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! # Ok(()) //! # Ok(())
//! # } //! # }
@@ -45,6 +39,7 @@
//! To: Hei <hei@domain.tld> //! To: Hei <hei@domain.tld>
//! Subject: Happy new year //! Subject: Happy new year
//! Date: Sat, 12 Dec 2020 16:33:19 GMT //! Date: Sat, 12 Dec 2020 16:33:19 GMT
//! Content-Type: text/plain; charset=utf-8
//! Content-Transfer-Encoding: 7bit //! Content-Transfer-Encoding: 7bit
//! //!
//! Be happy! //! Be happy!
@@ -63,7 +58,7 @@
//! //!
//! ```rust //! ```rust
//! # use std::error::Error; //! # use std::error::Error;
//! use lettre::message::{header, Message, MultiPart, Part, SinglePart}; //! use lettre::message::{header, Message, MultiPart, SinglePart};
//! //!
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! let m = Message::builder() //! let m = Message::builder()
@@ -71,21 +66,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?) //! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year") //! .subject("Happy new year")
//! .multipart( //! .multipart(MultiPart::alternative_plain_html(
//! MultiPart::alternative() //! String::from("Hello, world! :)"),
//! .singlepart( //! String::from("<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>"),
//! SinglePart::builder() //! ))?;
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body(String::from("Hello, world! :)")),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>",
//! )),
//! ),
//! )?;
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```
@@ -124,9 +108,10 @@
//! //!
//! ```rust //! ```rust
//! # use std::error::Error; //! # use std::error::Error;
//! use lettre::message::{header, Body, Message, MultiPart, Part, SinglePart};
//! use std::fs; //! use std::fs;
//! //!
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
//!
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! let image = fs::read("docs/lettre.png")?; //! let image = fs::read("docs/lettre.png")?;
//! // this image_body can be cloned and reused between emails. //! // this image_body can be cloned and reused between emails.
@@ -144,47 +129,22 @@
//! MultiPart::mixed() //! MultiPart::mixed()
//! .multipart( //! .multipart(
//! MultiPart::alternative() //! MultiPart::alternative()
//! .singlepart( //! .singlepart(SinglePart::plain(String::from("Hello, world! :)")))
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body(String::from("Hello, world! :)")),
//! )
//! .multipart( //! .multipart(
//! MultiPart::related() //! MultiPart::related()
//! .singlepart( //! .singlepart(SinglePart::html(String::from(
//! SinglePart::builder()
//! .header(header::ContentType(
//! "text/html; charset=utf8".parse()?,
//! ))
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>", //! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
//! )))
//! .singlepart(
//! Attachment::new_inline(String::from("123"))
//! .body(image_body, "image/png".parse().unwrap()),
//! ),
//! ),
//! )
//! .singlepart(Attachment::new(String::from("example.rs")).body(
//! String::from("fn main() { println!(\"Hello, World!\") }"),
//! "text/plain".parse().unwrap(),
//! )), //! )),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("image/png".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Inline,
//! parameters: vec![],
//! })
//! .header(header::ContentId("<123>".into()))
//! .body(image_body),
//! ),
//! ),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Attachment,
//! parameters: vec![header::DispositionParam::Filename(
//! header::Charset::Ext("utf-8".into()),
//! None,
//! "example.rs".as_bytes().into(),
//! )],
//! })
//! .body(String::from("fn main() { println!(\"Hello, World!\") }")),
//! ),
//! )?; //! )?;
//! # Ok(()) //! # Ok(())
//! # } //! # }
@@ -238,25 +198,26 @@
//! ``` //! ```
//! </details> //! </details>
use std::{io::Write, iter, time::SystemTime};
pub use attachment::Attachment;
pub use body::{Body, IntoBody, MaybeString}; pub use body::{Body, IntoBody, MaybeString};
#[cfg(feature = "dkim")]
pub use dkim::*;
pub use mailbox::*; pub use mailbox::*;
pub use mimebody::*; pub use mimebody::*;
pub use mime; mod attachment;
mod body; mod body;
#[cfg(feature = "dkim")]
pub mod dkim;
pub mod header; pub mod header;
mod mailbox; mod mailbox;
mod mimebody; mod mimebody;
mod utf8_b;
use std::{convert::TryFrom, time::SystemTime};
use uuid::Uuid;
use crate::{ use crate::{
address::Envelope, address::Envelope,
message::header::{ContentTransferEncoding, EmailDate, Header, Headers, MailboxesHeader}, message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
Error as EmailError, Error as EmailError,
}; };
@@ -273,6 +234,7 @@ trait EmailFormat {
pub struct MessageBuilder { pub struct MessageBuilder {
headers: Headers, headers: Headers,
envelope: Option<Envelope>, envelope: Option<Envelope>,
drop_bcc: bool,
} }
impl MessageBuilder { impl MessageBuilder {
@@ -281,71 +243,41 @@ impl MessageBuilder {
Self { Self {
headers: Headers::new(), headers: Headers::new(),
envelope: None, envelope: None,
drop_bcc: true,
} }
} }
/// Set custom header to message
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(mut self, header: H) -> Self {
if self.headers.has::<H>() {
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
self
} else {
self.header(header)
}
}
/// Add `Date` header to message
///
/// Shortcut for `self.header(header::Date(date))`.
pub fn date(self, date: EmailDate) -> Self {
self.header(header::Date(date))
}
/// Set `Date` header using current date/time
///
/// Shortcut for `self.date(SystemTime::now())`.
pub fn date_now(self) -> Self {
self.date(SystemTime::now().into())
}
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
self.header(header::Subject(subject.into()))
}
/// Set `Mime-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
/// Not exposed as it is set by body methods
fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0)
}
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
///
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
///
/// Shortcut for `self.header(header::Sender(mbox))`.
pub fn sender(self, mbox: Mailbox) -> Self {
self.header(header::Sender(mbox))
}
/// Set or add mailbox to `From` header /// Set or add mailbox to `From` header
/// ///
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2). /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
/// ///
/// Shortcut for `self.mailbox(header::From(mbox))`. /// Shortcut for `self.mailbox(header::From(mbox))`.
pub fn from(self, mbox: Mailbox) -> Self { pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From(mbox.into())) self.mailbox(header::From::from(Mailboxes::from(mbox)))
}
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
///
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
///
/// Shortcut for `self.header(header::Sender(mbox))`.
pub fn sender(self, mbox: Mailbox) -> Self {
self.header(header::Sender::from(mbox))
}
/// Add `Date` header to message
///
/// Shortcut for `self.header(header::Date::new(st))`.
pub fn date(self, st: SystemTime) -> Self {
self.header(header::Date::new(st))
}
/// Set `Date` header using current date/time
///
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
/// if no date has been provided.
pub fn date_now(self) -> Self {
self.date(SystemTime::now())
} }
/// Set or add mailbox to `ReplyTo` header /// Set or add mailbox to `ReplyTo` header
@@ -381,16 +313,24 @@ impl MessageBuilder {
/// Set or add message id to [`In-Reply-To` /// Set or add message id to [`In-Reply-To`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn in_reply_to(self, id: String) -> Self { pub fn in_reply_to(self, id: String) -> Self {
self.header(header::InReplyTo(id)) self.header(header::InReplyTo::from(id))
} }
/// Set or add message id to [`References` /// Set or add message id to [`References`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn references(self, id: String) -> Self { pub fn references(self, id: String) -> Self {
self.header(header::References(id)) self.header(header::References::from(id))
} }
/// Set [Message-Id /// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
let s: String = subject.into();
self.header(header::Subject::from(s))
}
/// Set [Message-ID
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
/// ///
/// Should generally be inserted by the mail relay. /// Should generally be inserted by the mail relay.
@@ -399,28 +339,45 @@ impl MessageBuilder {
/// `<UUID@HOSTNAME>`. /// `<UUID@HOSTNAME>`.
pub fn message_id(self, id: Option<String>) -> Self { pub fn message_id(self, id: Option<String>) -> Self {
match id { match id {
Some(i) => self.header(header::MessageId(i)), Some(i) => self.header(header::MessageId::from(i)),
None => { None => {
#[cfg(feature = "hostname")] #[cfg(feature = "hostname")]
let hostname = hostname::get() let hostname = hostname::get()
.map_err(|_| ()) .map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ())) .and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string()); .unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
#[cfg(not(feature = "hostname"))] #[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string(); let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
self.header(header::MessageId( self.header(header::MessageId::from(
// https://tools.ietf.org/html/rfc5322#section-3.6.4 // https://tools.ietf.org/html/rfc5322#section-3.6.4
format!("<{}@{}>", Uuid::new_v4(), hostname), format!("<{}@{}>", make_message_id(), hostname),
)) ))
} }
} }
} }
/// Set [User-Agent /// Set [User-Agent
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004) /// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00)
pub fn user_agent(self, id: String) -> Self { pub fn user_agent(self, id: String) -> Self {
self.header(header::UserAgent(id)) self.header(header::UserAgent::from(id))
}
/// Set custom header to message
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
match self.headers.get::<H>() {
Some(mut header_) => {
header_.join_mailboxes(header);
self.header(header_)
}
None => self.header(header),
}
} }
/// Force specific envelope (by default it is derived from headers) /// Force specific envelope (by default it is derived from headers)
@@ -429,6 +386,20 @@ impl MessageBuilder {
self self
} }
/// Keep the `Bcc` header
///
/// 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 behavior.
pub fn keep_bcc(mut self) -> Self {
self.drop_bcc = false;
self
}
// TODO: High-level methods for attachments and embedded files // TODO: High-level methods for attachments and embedded files
/// Create message from body /// Create message from body
@@ -437,7 +408,7 @@ impl MessageBuilder {
// https://tools.ietf.org/html/rfc5322#section-3.6 // https://tools.ietf.org/html/rfc5322#section-3.6
// Insert Date if missing // Insert Date if missing
let res = if self.headers.get::<header::Date>().is_none() { let mut res = if self.headers.get::<header::Date>().is_none() {
self.date_now() self.date_now()
} else { } else {
self self
@@ -446,7 +417,7 @@ impl MessageBuilder {
// Fail is missing correct originator (Sender or From) // Fail is missing correct originator (Sender or From)
match res.headers.get::<header::From>() { match res.headers.get::<header::From>() {
Some(header::From(f)) => { Some(header::From(f)) => {
let from: Vec<Mailbox> = f.clone().into(); let from: Vec<Mailbox> = f.into();
if from.len() > 1 && res.headers.get::<header::Sender>().is_none() { if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
return Err(EmailError::TooManyFrom); return Err(EmailError::TooManyFrom);
} }
@@ -460,6 +431,12 @@ impl MessageBuilder {
Some(e) => e, Some(e) => e,
None => Envelope::try_from(&res.headers)?, None => Envelope::try_from(&res.headers)?,
}; };
if res.drop_bcc {
// Remove `Bcc` headers now the envelope is set
res.headers.remove::<header::Bcc>();
}
Ok(Message { Ok(Message {
headers: res.headers, headers: res.headers,
body, body,
@@ -473,7 +450,7 @@ impl MessageBuilder {
/// `Content-Transfer-Encoding`, based on the most efficient and valid encoding /// `Content-Transfer-Encoding`, based on the most efficient and valid encoding
/// for `body`. /// for `body`.
pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> { pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied(); let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
let body = body.into_body(maybe_encoding); let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding()); self.headers.set(body.encoding());
@@ -489,9 +466,19 @@ impl MessageBuilder {
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> { pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(MessageBody::Mime(Part::Single(part))) self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
} }
/// Set `MIME-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
/// Not exposed as it is set by body methods
fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0)
}
} }
/// Email message which can be formatted /// Email message which can be formatted
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Message { pub struct Message {
headers: Headers, headers: Headers,
@@ -516,6 +503,11 @@ impl Message {
&self.headers &self.headers
} }
/// Get a mutable reference to the headers
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}
/// Get `Message` envelope /// Get `Message` envelope
pub fn envelope(&self) -> &Envelope { pub fn envelope(&self) -> &Envelope {
&self.envelope &self.envelope
@@ -527,17 +519,89 @@ impl Message {
self.format(&mut out); self.format(&mut out);
out out
} }
#[cfg(feature = "dkim")]
/// Format body for signing
pub(crate) fn body_raw(&self) -> Vec<u8> {
let mut out = Vec::new();
match &self.body {
MessageBody::Mime(p) => p.format_body(&mut out),
MessageBody::Raw(r) => out.extend_from_slice(r),
};
out.extend_from_slice(b"\r\n");
out
}
/// Sign the message using Dkim
///
/// Example:
/// ```rust
/// use lettre::{
/// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
/// Message,
/// };
///
/// let mut message = Message::builder()
/// .from("Alice <alice@example.org>".parse().unwrap())
/// .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_owned())
/// .unwrap();
/// let key = "-----BEGIN RSA PRIVATE KEY-----
/// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
/// NLTdc9xfPIOK8l/xGrN7Nd63J4cTATqZukumczkA46O8YKHwa53pNT6NYwCNtDUL
/// eBu+7xUW18GmDzkIFkxGO2R5kkTeWPlKvKpEiicIMfl0OmyW/fI3AbtM7e/gmqQ4
/// kEYIO0mTjPT+jTgWE4JIi5KUTHudUBtfMKcSFyM2HkUOExl1c9+A4epjRFQwEXMA
/// hM5GrqZoOdUm4fIpvGpLIGIxFgHPpZYbyq6yJZzH3+5aKyCHrsHawPuPiCD45zsU
/// re31zCE6b6k1sDiiBR4CaRHnbL7hxFp0aNLOVQIDAQABAoIBAGMK3gBrKxaIcUGo
/// gQeIf7XrJ6vK72YC9L8uleqI4a9Hy++E7f4MedZ6eBeWta8jrnEL4Yp6xg+beuDc
/// A24+Mhng+6Dyp+TLLqj+8pQlPnbrMprRVms7GIXFrrs+wO1RkBNyhy7FmH0roaMM
/// pJZzoGW2pE9QdbqjL3rdlWTi/60xRX9eZ42nNxYnbc+RK03SBd46c3UBha6Y9iQX
/// 562yWilDnB5WCX2tBoSN39bEhJvuZDzMwOuGw68Q96Hdz82Iz1xVBnRhH+uNStjR
/// VnAssSHVxPSpwWrm3sHlhjBHWPnNIaOKIKl1lbL+qWfVQCj/6a5DquC+vYAeYR6L
/// 3mA0z0ECgYEA5YkNYcILSXyE0hZ8eA/t58h8eWvYI5iqt3nT4fznCoYJJ74Vukeg
/// 6BTlq/CsanwT1lDtvDKrOaJbA7DPTES/bqT0HoeIdOvAw9w/AZI5DAqYp61i6RMK
/// xfAQL/Ik5MDFN8gEMLLXRVMe/aR27f6JFZpShJOK/KCzHqikKfYVJ+UCgYEAzI2F
/// ZlTyittWSyUSl5UKyfSnFOx2+6vNy+lu5DeMJu8Wh9rqBk388Bxq98CfkCseWESN
/// pTCGdYltz9DvVNBdBLwSMdLuYJAI6U+Zd70MWyuNdHFPyWVHUNqMUBvbUtj2w74q
/// Hzu0GI0OrRjdX6C63S17PggmT/N2R9X7P4STxbECgYA+AZAD4I98Ao8+0aQ+Ks9x
/// 1c8KXf+9XfiAKAD9A3zGcv72JXtpHwBwsXR5xkJNYcdaFfKi7G0k3J8JmDHnwIqW
/// MSlhNeu+6hDg2BaNLhsLDbG/Wi9mFybJ4df9m8Qrp4efUgEPxsAwkgvFKTCXijMu
/// CspP1iutoxvAJH50d22voQKBgDIsSFtIXNGYaTs3Va8enK3at5zXP3wNsQXiNRP/
/// V/44yNL77EktmewfXFF2yuym1uOZtRCerWxpEClYO0wXa6l8pA3aiiPfUIBByQfo
/// s/4s2Z6FKKfikrKPWLlRi+NvWl+65kQQ9eTLvJzSq4IIP61+uWsGvrb/pbSLFPyI
/// fWKRAoGBALFCStBXvdMptjq4APUzAdJ0vytZzXkOZHxgmc+R0fQn22OiW0huW6iX
/// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw
/// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25
/// -----END RSA PRIVATE KEY-----";
/// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
/// message.sign(&DkimConfig::default_config(
/// "dkimtest".to_owned(),
/// "example.org".to_owned(),
/// signing_key,
/// ));
/// println!(
/// "message: {}",
/// std::str::from_utf8(&message.formatted()).unwrap()
/// );
/// ```
#[cfg(feature = "dkim")]
pub fn sign(&mut self, dkim_config: &DkimConfig) {
dkim_sign(self, dkim_config);
}
} }
impl EmailFormat for Message { impl EmailFormat for Message {
fn format(&self, out: &mut Vec<u8>) { fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes()); write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
match &self.body { match &self.body {
MessageBody::Mime(p) => p.format(out), MessageBody::Mime(p) => p.format(out),
MessageBody::Raw(r) => { MessageBody::Raw(r) => {
out.extend_from_slice(b"\r\n"); out.extend_from_slice(b"\r\n");
out.extend_from_slice(&r) out.extend_from_slice(r)
} }
} }
} }
@@ -549,9 +613,19 @@ impl Default for MessageBuilder {
} }
} }
/// Create a random message id.
/// (Not cryptographically random)
fn make_message_id() -> String {
iter::repeat_with(fastrand::alphanumeric).take(36).collect()
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::message::{header, mailbox::Mailbox, Message, MultiPart, SinglePart}; use std::time::{Duration, SystemTime};
use pretty_assertions::assert_eq;
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
#[test] #[test]
fn email_missing_originator() { fn email_missing_originator() {
@@ -561,7 +635,7 @@ mod test {
} }
#[test] #[test]
fn email_miminal_message() { fn email_minimal_message() {
assert!(Message::builder() assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("NoBody <nobody@domain.tld>".parse().unwrap()) .to("NoBody <nobody@domain.tld>".parse().unwrap())
@@ -579,11 +653,13 @@ mod test {
} }
#[test] #[test]
fn email_message() { fn email_message_no_bcc() {
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap(); // Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
let email = Message::builder() let email = Message::builder()
.date(date) .date(date)
.bcc("hidden@example.com".parse().unwrap())
.header(header::From( .header(header::From(
vec![Mailbox::new( vec![Mailbox::new(
Some("Каи".into()), Some("Каи".into()),
@@ -594,16 +670,54 @@ mod test {
.header(header::To( .header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(), vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
)) ))
.header(header::Subject("яңа ел белән!".into())) .header(header::Subject::from(String::from("яңа ел белән!")))
.body(String::from("Happy new year!")) .body(String::from("Happy new year!"))
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
String::from_utf8(email.formatted()).unwrap(), String::from_utf8(email.formatted()).unwrap(),
concat!( concat!(
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n", "From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: Pony O.P. <pony@domain.tld>\r\n", "To: \"Pony O.P.\" <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Happy new year!"
)
);
}
#[test]
fn email_message_keep_bcc() {
// Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
let email = Message::builder()
.date(date)
.bcc("hidden@example.com".parse().unwrap())
.keep_bcc()
.header(header::From(
vec![Mailbox::new(
Some("Каи".into()),
"kayo@example.com".parse().unwrap(),
)]
.into(),
))
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject::from(String::from("яңа ел белән!")))
.body(String::from("Happy new year!"))
.unwrap();
assert_eq!(
String::from_utf8(email.formatted()).unwrap(),
concat!(
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Bcc: hidden@example.com\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n", "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
@@ -614,7 +728,8 @@ mod test {
#[test] #[test]
fn email_with_png() { fn email_with_png() {
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap(); // Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
let img = std::fs::read("./docs/lettre.png").unwrap(); let img = std::fs::read("./docs/lettre.png").unwrap();
let m = Message::builder() let m = Message::builder()
.date(date) .date(date)
@@ -626,21 +741,16 @@ mod test {
MultiPart::related() MultiPart::related()
.singlepart( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(header::ContentType( .header(header::ContentType::TEXT_HTML)
"text/html; charset=utf8".parse().unwrap(),
))
.body(String::from( .body(String::from(
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>", "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
)), )),
) )
.singlepart( .singlepart(
SinglePart::builder() SinglePart::builder()
.header(header::ContentType("image/png".parse().unwrap())) .header(header::ContentType::parse("image/png").unwrap())
.header(header::ContentDisposition { .header(header::ContentDisposition::inline())
disposition: header::DispositionType::Inline, .header(header::ContentId::from(String::from("<123>")))
parameters: vec![],
})
.header(header::ContentId("<123>".into()))
.body(img), .body(img),
), ),
) )
@@ -651,11 +761,27 @@ mod test {
let expected = String::from_utf8(file_expected).unwrap(); let expected = String::from_utf8(file_expected).unwrap();
for (i, line) in output.lines().zip(expected.lines()).enumerate() { for (i, line) in output.lines().zip(expected.lines()).enumerate() {
if i == 6 || i == 8 || i == 13 || i == 232 { if i == 7 || i == 9 || i == 14 || i == 233 {
continue; continue;
} }
assert_eq!(line.0, line.1) assert_eq!(line.0, line.1)
} }
} }
#[test]
fn test_make_message_id() {
let mut ids = std::collections::HashSet::with_capacity(10);
for _ in 0..1000 {
ids.insert(make_message_id());
}
// Ensure there are no duplicates
assert_eq!(1000, ids.len());
// Ensure correct length
for id in ids {
assert_eq!(36, id.len());
}
}
} }

View File

@@ -1,60 +0,0 @@
// https://tools.ietf.org/html/rfc1522
fn allowed_char(c: char) -> bool {
c >= 1 as char && c <= 9 as char
|| c == 11 as char
|| c == 12 as char
|| c >= 14 as char && c <= 127 as char
}
pub fn encode(s: &str) -> String {
if s.chars().all(allowed_char) {
s.into()
} else {
format!("=?utf-8?b?{}?=", base64::encode(s))
}
}
pub fn decode(s: &str) -> Option<String> {
s.strip_prefix("=?utf-8?b?")
.and_then(|stripped| stripped.strip_suffix("?="))
.map_or_else(
|| Some(s.into()),
|stripped| {
let decoded = base64::decode(stripped).ok()?;
let decoded = String::from_utf8(decoded).ok()?;
Some(decoded)
},
)
}
#[cfg(test)]
mod test {
use super::{decode, encode};
#[test]
fn encode_ascii() {
assert_eq!(&encode("Kayo. ?"), "Kayo. ?");
}
#[test]
fn decode_ascii() {
assert_eq!(decode("Kayo. ?"), Some("Kayo. ?".into()));
}
#[test]
fn encode_utf8() {
assert_eq!(
&encode("Привет, мир!"),
"=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="
);
}
#[test]
fn decode_utf8() {
assert_eq!(
decode("=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="),
Some("Привет, мир!".into())
);
}
}

View File

@@ -1,61 +1,97 @@
//! Error and result type for file transport //! Error and result type for file transport
use self::Error::*; use std::{error::Error as StdError, fmt};
use std::{
error::Error as StdError, use crate::BoxError;
fmt::{self, Display, Formatter},
io, /// The Errors that may occur when sending an email over SMTP
pub struct Error {
inner: Box<Inner>,
}
struct Inner {
kind: Kind,
source: Option<BoxError>,
}
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
}),
}
}
/// Returns true if the error is a file I/O error
pub fn is_io(&self) -> bool {
matches!(self.inner.kind, Kind::Io)
}
/// Returns true if the error is an envelope serialization or deserialization error
#[cfg(feature = "file-transport-envelope")]
pub fn is_envelope(&self) -> bool {
matches!(self.inner.kind, Kind::Envelope)
}
}
#[derive(Debug)]
pub(crate) enum Kind {
/// File I/O error
Io,
/// Envelope serialization/deserialization error
#[cfg(feature = "file-transport-envelope")]
Envelope,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("lettre::transport::file::Error");
builder.field("kind", &self.inner.kind);
if let Some(source) = &self.inner.source {
builder.field("source", source);
}
builder.finish()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner.kind {
Kind::Io => f.write_str("response error")?,
#[cfg(feature = "file-transport-envelope")]
Kind::Envelope => f.write_str("internal client error")?,
}; };
/// An enum of all error kinds. if let Some(e) = &self.inner.source {
#[derive(Debug)] write!(f, ": {e}")?;
pub enum Error {
/// Internal client error
Client(&'static str),
/// IO error
Io(io::Error),
/// JSON error
#[cfg(feature = "file-transport-envelope")]
Json(serde_json::Error),
} }
impl Display for Error { Ok(())
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
#[cfg(feature = "file-transport-envelope")]
Json(ref err) => err.fmt(fmt),
}
} }
} }
impl StdError for Error { impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self { self.inner.source.as_ref().map(|e| {
Io(ref err) => Some(&*err), let r: &(dyn std::error::Error + 'static) = &**e;
#[cfg(feature = "file-transport-envelope")] r
Json(ref err) => Some(&*err), })
_ => None,
}
} }
} }
impl From<io::Error> for Error { pub(crate) fn io<E: Into<BoxError>>(e: E) -> Error {
fn from(err: io::Error) -> Error { Error::new(Kind::Io, Some(e))
Error::Io(err)
}
} }
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
impl From<serde_json::Error> for Error { pub(crate) fn envelope<E: Into<BoxError>>(e: E) -> Error {
fn from(err: serde_json::Error) -> Error { Error::new(Kind::Envelope, Some(e))
Error::Json(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client(string)
}
} }

View File

@@ -6,12 +6,13 @@
//! //!
//! ```rust //! ```rust
//! # use std::error::Error; //! # use std::error::Error;
//! //! #
//! # #[cfg(all(feature = "file-transport", feature = "builder"))] //! # #[cfg(all(feature = "file-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! //!
//! use lettre::{FileTransport, Message, Transport};
//!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir()); //! let sender = FileTransport::new(temp_dir());
//! let email = Message::builder() //! let email = Message::builder()
@@ -38,12 +39,13 @@
//! //!
//! ```rust //! ```rust
//! # use std::error::Error; //! # use std::error::Error;
//! //! #
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))] //! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! //!
//! use lettre::{FileTransport, Message, Transport};
//!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = FileTransport::with_envelope(temp_dir()); //! let sender = FileTransport::with_envelope(temp_dir());
//! let email = Message::builder() //! let email = Message::builder()
@@ -66,11 +68,12 @@
//! //!
//! ```rust,no_run //! ```rust,no_run
//! # use std::error::Error; //! # use std::error::Error;
//! //! #
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))] //! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! use lettre::{AsyncTransport, Tokio1Executor, Message, AsyncFileTransport}; //!
//! use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
//! //!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir()); //! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
@@ -91,11 +94,12 @@
//! //!
//! ```rust,no_run //! ```rust,no_run
//! # use std::error::Error; //! # use std::error::Error;
//! //! #
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))] //! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! use lettre::{AsyncTransport, AsyncStd1Executor, Message, AsyncFileTransport}; //!
//! use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
//! //!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir()); //! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
@@ -132,36 +136,41 @@
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"} //! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"}
//! ``` //! ```
pub use self::error::Error; #[cfg(any(feature = "async-std1", feature = "tokio1"))]
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use std::marker::PhantomData; use std::marker::PhantomData;
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
str, str,
}; };
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
use uuid::Uuid; use uuid::Uuid;
pub use self::error::Error;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
mod error; mod error;
type Id = String; type Id = String;
/// Writes the content and the envelope information to a file /// 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(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
pub struct FileTransport { pub struct FileTransport {
path: PathBuf, path: PathBuf,
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
save_envelope: bool, save_envelope: bool,
} }
#[derive(Debug)] /// Asynchronously writes the content and the envelope information to a file
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
pub struct AsyncFileTransport<E: Executor> { pub struct AsyncFileTransport<E: Executor> {
inner: FileTransport, inner: FileTransport,
marker_: PhantomData<E>, marker_: PhantomData<E>,
@@ -199,22 +208,22 @@ impl FileTransport {
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> { pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
use std::fs; use std::fs;
let eml_file = self.path.join(format!("{}.eml", email_id)); let eml_file = self.path.join(format!("{email_id}.eml"));
let eml = fs::read(eml_file)?; let eml = fs::read(eml_file).map_err(error::io)?;
let json_file = self.path.join(format!("{}.json", email_id)); let json_file = self.path.join(format!("{email_id}.json"));
let json = fs::read(&json_file)?; let json = fs::read(json_file).map_err(error::io)?;
let envelope = serde_json::from_slice(&json)?; let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
Ok((envelope, eml)) Ok((envelope, eml))
} }
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf { fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
self.path.join(format!("{}.{}", email_id, extension)) self.path.join(format!("{email_id}.{extension}"))
} }
} }
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl<E> AsyncFileTransport<E> impl<E> AsyncFileTransport<E>
where where
E: Executor, E: Executor,
@@ -246,12 +255,12 @@ where
/// Reads the envelope and the raw message content. /// Reads the envelope and the raw message content.
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> { pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
let eml_file = self.inner.path.join(format!("{}.eml", email_id)); let eml_file = self.inner.path.join(format!("{email_id}.eml"));
let eml = E::fs_read(&eml_file).await?; let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
let json_file = self.inner.path.join(format!("{}.json", email_id)); let json_file = self.inner.path.join(format!("{email_id}.json"));
let json = E::fs_read(&json_file).await?; let json = E::fs_read(&json_file).await.map_err(error::io)?;
let envelope = serde_json::from_slice(&json)?; let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
Ok((envelope, eml)) Ok((envelope, eml))
} }
@@ -267,13 +276,16 @@ impl Transport for FileTransport {
let email_id = Uuid::new_v4(); let email_id = Uuid::new_v4();
let file = self.path(&email_id, "eml"); let file = self.path(&email_id, "eml");
fs::write(file, email)?; #[cfg(feature = "tracing")]
tracing::debug!(?file, "writing email to");
fs::write(file, email).map_err(error::io)?;
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
{ {
if self.save_envelope { if self.save_envelope {
let file = self.path(&email_id, "json"); let file = self.path(&email_id, "json");
fs::write(file, serde_json::to_string(&envelope)?)?; let buf = serde_json::to_string(&envelope).map_err(error::envelope)?;
fs::write(file, buf).map_err(error::io)?;
} }
} }
// use envelope anyway // use envelope anyway
@@ -283,7 +295,7 @@ impl Transport for FileTransport {
} }
} }
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]
#[async_trait] #[async_trait]
impl<E> AsyncTransport for AsyncFileTransport<E> impl<E> AsyncTransport for AsyncFileTransport<E>
where where
@@ -296,14 +308,16 @@ where
let email_id = Uuid::new_v4(); let email_id = Uuid::new_v4();
let file = self.inner.path(&email_id, "eml"); let file = self.inner.path(&email_id, "eml");
E::fs_write(&file, email).await?; #[cfg(feature = "tracing")]
tracing::debug!(?file, "writing email to");
E::fs_write(&file, email).await.map_err(error::io)?;
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
{ {
if self.inner.save_envelope { if self.inner.save_envelope {
let file = self.inner.path(&email_id, "json"); let file = self.inner.path(&email_id, "json");
let buf = serde_json::to_vec(&envelope)?; let buf = serde_json::to_vec(&envelope).map_err(error::envelope)?;
E::fs_write(&file, &buf).await?; E::fs_write(&file, &buf).await.map_err(error::io)?;
} }
} }
// use envelope anyway // use envelope anyway

View File

@@ -1,37 +1,107 @@
//! ### Sending Messages //! ## Transports for sending emails
//! //!
//! This section explains how to manipulate emails you have created. //! This module contains `Transport`s for sending emails. A `Transport` implements a high-level API
//! for sending emails. It automatically manages the underlying resources and doesn't require any
//! specific knowledge of email protocols in order to be used.
//! //!
//! This mailer contains several different transports for your emails. To be sendable, the //! ### Getting started
//! emails have to implement `Email`, which is the case for emails created with `lettre::builder`. //!
//! Sending emails from your programs requires using an email relay, as client libraries are not
//! designed to handle email delivery by themselves. Depending on your infrastructure, your relay
//! could be:
//!
//! * a service from your Cloud or hosting provider
//! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either
//! locally on your servers or accessible over the network
//! * a dedicated external service, like Mailchimp, Mailgun, etc.
//!
//! In most cases, the best option is to:
//!
//! * Use the [`SMTP`] transport, with the [`relay`] builder (or one of its async counterparts)
//! with your server's hostname. They provide modern and secure defaults.
//! * Use the [`credentials`] method of the builder to pass your credentials.
//!
//! These should be enough to safely cover most use cases.
//!
//! ### Available transports
//! //!
//! The following transports are available: //! The following transports are available:
//! //!
//! * The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is //! | Module | Protocol | Sync API | Async API | Description |
//! the preferred way of sending emails. //! | ------------ | -------- | --------------------- | -------------------------- | ------------------------------------------------------- |
//! * The `SendmailTransport` uses the sendmail command to send messages. It is an alternative to //! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
//! the SMTP transport. //! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
//! * The `FileTransport` creates a file containing the email content to be sent. It can be used //! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
//! for debugging or if you want to keep all sent emails. //! | [`stub`] | Debug | [`StubTransport`] | [`AsyncStubTransport`] | Drops the email - Useful for debugging |
//! * The `StubTransport` is useful for debugging, and only prints the content of the email in the //!
//! logs. //! ## Building an email
//!
//! Emails can either be built though [`Message`], which is a typed API for constructing emails
//! (find out more about it by going over the [`message`][crate::message] module),
//! or via external means.
//!
//! [`Message`]s can be sent via [`Transport::send`] or [`AsyncTransport::send`], while messages
//! built without lettre's [`message`][crate::message] APIs can be sent via [`Transport::send_raw`]
//! or [`AsyncTransport::send_raw`].
//!
//! ## Brief example
//!
//! This example shows how to build an email and send it via an SMTP relay server.
//! It is in no way a complete example, but it shows how to get started with lettre.
//! More examples can be found by looking at the specific modules, linked in the _Module_ column
//! of the [table above](#transports-for-sending-emails).
//!
//! ```rust,no_run
//! # use std::error::Error;
//! #
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! 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")?
//! .credentials(creds)
//! .build();
//!
//! // Send the email
//! match mailer.send(&email) {
//! Ok(_) => println!("Email sent successfully!"),
//! Err(e) => panic!("Could not send email: {e:?}"),
//! }
//! # Ok(())
//! # }
//! # #[cfg(not(all(feature = "builder", feature = "smtp-transport")))]
//! # fn main() {}
//! ```
//!
//! [MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent
//! [`SMTP`]: crate::transport::smtp
//! [`relay`]: crate::SmtpTransport::relay
//! [`starttls_relay`]: crate::SmtpTransport::starttls_relay
//! [`credentials`]: crate::transport::smtp::SmtpTransportBuilder::credentials
//! [`Message`]: crate::Message
//! [`file`]: self::file
//! [`SmtpTransport`]: crate::SmtpTransport
//! [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
//! [`SendmailTransport`]: crate::SendmailTransport
//! [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
//! [`FileTransport`]: crate::FileTransport
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
//! [`StubTransport`]: crate::transport::stub::StubTransport
//! [`AsyncStubTransport`]: crate::transport::stub::AsyncStubTransport
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait; use async_trait::async_trait;
#[doc(hidden)]
#[deprecated(note = "use lettre::AsyncStd1Transport")]
#[cfg(feature = "async-std1")]
pub use self::AsyncTransport as AsyncStd1Transport;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio1Transport")]
#[cfg(feature = "tokio1")]
pub use self::AsyncTransport as Tokio1Transport;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio02Transport")]
#[cfg(feature = "tokio02")]
pub use self::AsyncTransport as Tokio02Transport;
use crate::Envelope; use crate::Envelope;
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
use crate::Message; use crate::Message;
@@ -58,6 +128,9 @@ pub trait Transport {
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))] #[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> { fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "tracing")]
tracing::trace!("starting to send an email");
let raw = message.formatted(); let raw = message.formatted();
self.send_raw(message.envelope(), &raw) self.send_raw(message.envelope(), &raw)
} }
@@ -66,11 +139,8 @@ pub trait Transport {
} }
/// Async Transport method for emails /// Async Transport method for emails
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[cfg_attr( #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[async_trait] #[async_trait]
pub trait AsyncTransport { pub trait AsyncTransport {
/// Response produced by the Transport /// Response produced by the Transport
@@ -83,9 +153,12 @@ pub trait AsyncTransport {
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))] #[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message // TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> { async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "tracing")]
tracing::trace!("starting to send an email");
let raw = message.formatted(); let raw = message.formatted();
let envelope = message.envelope(); let envelope = message.envelope();
self.send_raw(&envelope, &raw).await self.send_raw(envelope, &raw).await
} }
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>; async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;

View File

@@ -1,52 +1,93 @@
//! Error and result type for sendmail transport //! Error and result type for sendmail transport
use self::Error::*; use std::{error::Error as StdError, fmt};
use std::{
error::Error as StdError, use crate::BoxError;
fmt::{self, Display, Formatter},
io, /// The Errors that may occur when sending an email over sendmail
string::FromUtf8Error, pub struct Error {
inner: Box<Inner>,
}
struct Inner {
kind: Kind,
source: Option<BoxError>,
}
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
}),
}
}
/// Returns true if the error is from client
pub fn is_client(&self) -> bool {
matches!(self.inner.kind, Kind::Client)
}
/// Returns true if the error comes from the response
pub fn is_response(&self) -> bool {
matches!(self.inner.kind, Kind::Response)
}
}
#[derive(Debug)]
pub(crate) enum Kind {
/// Error parsing a response
Response,
/// Internal client error
Client,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("lettre::transport::sendmail::Error");
builder.field("kind", &self.inner.kind);
if let Some(source) = &self.inner.source {
builder.field("source", source);
}
builder.finish()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner.kind {
Kind::Response => f.write_str("response error")?,
Kind::Client => f.write_str("internal client error")?,
}; };
/// An enum of all error kinds. if let Some(e) = &self.inner.source {
#[derive(Debug)] write!(f, ": {e}")?;
pub enum Error {
/// Internal client error
Client(String),
/// Error parsing UTF8 in response
Utf8Parsing(FromUtf8Error),
/// IO error
Io(io::Error),
} }
impl Display for Error { Ok(())
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Io(ref err) => err.fmt(fmt),
}
} }
} }
impl StdError for Error { impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self { self.inner.source.as_ref().map(|e| {
Io(ref err) => Some(&*err), let r: &(dyn std::error::Error + 'static) = &**e;
Utf8Parsing(ref err) => Some(&*err), r
_ => None, })
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
} }
} }
impl From<FromUtf8Error> for Error { pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
fn from(err: FromUtf8Error) -> Error { Error::new(Kind::Response, Some(e))
Utf8Parsing(err)
} }
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Client, Some(e))
} }

View File

@@ -1,13 +1,13 @@
//! The sendmail transport sends the email using the local sendmail command. //! The sendmail transport sends the email using the local `sendmail` command.
//! //!
//! ## Sync example //! ## Sync example
//! //!
//! ```rust //! ```rust
//! # use std::error::Error; //! # use std::error::Error;
//! //! #
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))] //! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! # use lettre::{Message, Transport, SendmailTransport}; //! use lettre::{Message, SendmailTransport, Transport};
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -26,37 +26,16 @@
//! # fn main() {} //! # fn main() {}
//! ``` //! ```
//! //!
//! ## Async tokio 0.2 example
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "tokio02", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, Tokio02Executor, AsyncSendmailTransport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let sender = AsyncSendmailTransport::<Tokio02Executor>::new();
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! ## Async tokio 1.x example //! ## Async tokio 1.x example
//! //!
//! ```rust,no_run //! ```rust,no_run
//! # use std::error::Error; //! # use std::error::Error;
//! //! #
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))] //! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, Tokio1Executor, AsyncSendmailTransport, SendmailTransport}; //! use lettre::{
//! AsyncSendmailTransport, AsyncTransport, Message, SendmailTransport, Tokio1Executor,
//! };
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -76,7 +55,7 @@
//! //!
//!```rust,no_run //!```rust,no_run
//! # use std::error::Error; //! # use std::error::Error;
//! //! #
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))] //! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport}; //! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport};
@@ -95,50 +74,56 @@
//! # } //! # }
//! ``` //! ```
pub use self::error::Error; #[cfg(any(feature = "async-std1", feature = "tokio1"))]
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Executor;
#[cfg(feature = "tokio02")]
use crate::Tokio02Executor;
#[cfg(feature = "tokio1")]
use crate::Tokio1Executor;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use std::marker::PhantomData; use std::marker::PhantomData;
use std::{ use std::{
ffi::OsString, ffi::OsString,
io::prelude::*, io::Write,
process::{Command, Stdio}, process::{Command, Stdio},
}; };
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
pub use self::error::Error;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Executor;
#[cfg(feature = "tokio1")]
use crate::Tokio1Executor;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
mod error; mod error;
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail"; const DEFAULT_SENDMAIL: &str = "sendmail";
/// Sends an email using the `sendmail` command /// Sends emails using the `sendmail` command
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
pub struct SendmailTransport { pub struct SendmailTransport {
command: OsString, command: OsString,
} }
/// Asynchronously sends emails using the `sendmail` command
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSendmailTransport<E: Executor> { pub struct AsyncSendmailTransport<E: Executor> {
inner: SendmailTransport, inner: SendmailTransport,
marker_: PhantomData<E>, marker_: PhantomData<E>,
} }
impl SendmailTransport { impl SendmailTransport {
/// Creates a new transport with the default `/usr/sbin/sendmail` command /// Creates a new transport with the `sendmail` command
///
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
/// use [SendmailTransport::new_with_command].
pub fn new() -> SendmailTransport { pub fn new() -> SendmailTransport {
SendmailTransport { SendmailTransport {
command: DEFAUT_SENDMAIL.into(), command: DEFAULT_SENDMAIL.into(),
} }
} }
@@ -164,12 +149,15 @@ impl SendmailTransport {
} }
} }
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl<E> AsyncSendmailTransport<E> impl<E> AsyncSendmailTransport<E>
where where
E: Executor, E: Executor,
{ {
/// Creates a new transport with the default `/usr/sbin/sendmail` command /// Creates a new transport with the `sendmail` command
///
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
/// use [AsyncSendmailTransport::new_with_command].
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
inner: SendmailTransport::new(), inner: SendmailTransport::new(),
@@ -185,24 +173,6 @@ where
} }
} }
#[cfg(feature = "tokio02")]
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
use tokio02_crate::process::Command;
let mut c = Command::new(&self.inner.command);
c.kill_on_drop(true);
c.arg("-i");
if let Some(from) = envelope.from() {
c.arg("-f").arg(from);
}
c.arg("--")
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
c
}
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command { fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
use tokio1_crate::process::Command; use tokio1_crate::process::Command;
@@ -247,7 +217,7 @@ impl Default for SendmailTransport {
} }
} }
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl<E> Default for AsyncSendmailTransport<E> impl<E> Default for AsyncSendmailTransport<E>
where where
E: Executor, E: Executor,
@@ -262,16 +232,25 @@ impl Transport for SendmailTransport {
type Error = Error; type Error = Error;
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> { fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
// Spawn the sendmail command #[cfg(feature = "tracing")]
let mut process = self.command(envelope).spawn()?; tracing::debug!(command = ?self.command, "sending email with");
process.stdin.as_mut().unwrap().write_all(email)?; // Spawn the sendmail command
let output = process.wait_with_output()?; let mut process = self.command(envelope).spawn().map_err(error::client)?;
process
.stdin
.as_mut()
.unwrap()
.write_all(email)
.map_err(error::client)?;
let output = process.wait_with_output().map_err(error::client)?;
if output.status.success() { if output.status.success() {
Ok(()) Ok(())
} else { } else {
Err(error::Error::Client(String::from_utf8(output.stderr)?)) let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
} }
} }
} }
@@ -285,43 +264,28 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> { async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use async_std::io::prelude::WriteExt; use async_std::io::prelude::WriteExt;
#[cfg(feature = "tracing")]
tracing::debug!(command = ?self.inner.command, "sending email with");
let mut command = self.async_std_command(envelope); let mut command = self.async_std_command(envelope);
// Spawn the sendmail command // Spawn the sendmail command
let mut process = command.spawn()?; let mut process = command.spawn().map_err(error::client)?;
process.stdin.as_mut().unwrap().write_all(&email).await?; process
let output = process.output().await?; .stdin
.as_mut()
.unwrap()
.write_all(email)
.await
.map_err(error::client)?;
let output = process.output().await.map_err(error::client)?;
if output.status.success() { if output.status.success() {
Ok(()) Ok(())
} else { } else {
Err(Error::Client(String::from_utf8(output.stderr)?)) let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
} Err(error::client(stderr))
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl AsyncTransport for AsyncSendmailTransport<Tokio02Executor> {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio02_crate::io::AsyncWriteExt;
let mut command = self.tokio02_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.wait_with_output().await?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
} }
} }
} }
@@ -335,18 +299,28 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> { async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio1_crate::io::AsyncWriteExt; use tokio1_crate::io::AsyncWriteExt;
#[cfg(feature = "tracing")]
tracing::debug!(command = ?self.inner.command, "sending email with");
let mut command = self.tokio1_command(envelope); let mut command = self.tokio1_command(envelope);
// Spawn the sendmail command // Spawn the sendmail command
let mut process = command.spawn()?; let mut process = command.spawn().map_err(error::client)?;
process.stdin.as_mut().unwrap().write_all(&email).await?; process
let output = process.wait_with_output().await?; .stdin
.as_mut()
.unwrap()
.write_all(email)
.await
.map_err(error::client)?;
let output = process.wait_with_output().await.map_err(error::client)?;
if output.status.success() { if output.status.success() {
Ok(()) Ok(())
} else { } else {
Err(Error::Client(String::from_utf8(output.stderr)?)) let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
} }
} }
} }

View File

@@ -1,44 +1,37 @@
use std::marker::PhantomData; #[cfg(feature = "pool")]
use std::sync::Arc;
use std::{
fmt::{self, Debug},
marker::PhantomData,
time::Duration,
};
use async_trait::async_trait; use async_trait::async_trait;
#[cfg(feature = "pool")]
use super::pool::async_impl::Pool;
#[cfg(feature = "pool")]
use super::PoolConfig;
use super::{ use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo, client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
}; };
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
use crate::AsyncStd1Executor; use crate::AsyncStd1Executor;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport; use crate::AsyncTransport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Executor;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
use crate::Tokio1Executor; use crate::Tokio1Executor;
use crate::{Envelope, Executor}; use crate::{Envelope, Executor};
#[allow(missing_debug_implementations)] /// Asynchronously sends emails using the SMTP protocol
pub struct AsyncSmtpTransport<E> { #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
// TODO: pool pub struct AsyncSmtpTransport<E: Executor> {
#[cfg(feature = "pool")]
inner: Arc<Pool<E>>,
#[cfg(not(feature = "pool"))]
inner: AsyncSmtpClient<E>, inner: AsyncSmtpClient<E>,
} }
#[cfg(feature = "tokio02")]
#[async_trait]
impl AsyncTransport for AsyncSmtpTransport<Tokio02Executor> {
type Ok = Response;
type Error = Error;
/// Sends an email
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let mut conn = self.inner.connection().await?;
let result = conn.send(envelope, email).await?;
conn.quit().await?;
Ok(result)
}
}
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
#[async_trait] #[async_trait]
impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> { impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
@@ -51,6 +44,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?; let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))]
conn.quit().await?; conn.quit().await?;
Ok(result) Ok(result)
@@ -86,13 +80,19 @@ where
/// Creates an encrypted transport over submissions port, using the provided domain /// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates. /// to validate TLS certificates.
#[cfg(any( #[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls", feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
)))
)]
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> { pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{Tls, TlsParameters, SUBMISSIONS_PORT}; use super::{Tls, TlsParameters, SUBMISSIONS_PORT};
@@ -103,7 +103,7 @@ where
.tls(Tls::Wrapper(tls_parameters))) .tls(Tls::Wrapper(tls_parameters)))
} }
/// Simple an secure transport, using STARTTLS to obtain encrypted connections /// Simple and secure transport, using STARTTLS to obtain encrypted connections
/// ///
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers /// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections. /// that don't take SMTPS connections.
@@ -115,13 +115,19 @@ where
/// An error is returned if the connection can't be upgraded. No credentials /// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks. /// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any( #[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls", feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
)))
)]
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> { pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{Tls, TlsParameters, SUBMISSION_PORT}; use super::{Tls, TlsParameters, SUBMISSION_PORT};
@@ -145,17 +151,131 @@ where
/// ///
/// * No authentication /// * No authentication
/// * No TLS /// * No TLS
/// * A 60-seconds timeout for smtp commands
/// * Port 25 /// * Port 25
/// ///
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or /// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead, /// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible. /// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder { pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
let new = SmtpInfo { AsyncSmtpTransportBuilder::new(server)
server: server.into(), }
..Default::default()
}; /// Creates a `AsyncSmtpTransportBuilder` from a connection URL
AsyncSmtpTransportBuilder { info: new } ///
/// 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 afterward if a connection pool is not used.
pub async fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection().await?;
let is_connected = conn.test_connected().await;
#[cfg(not(feature = "pool"))]
conn.quit().await?;
Ok(is_connected)
}
}
impl<E: Executor> Debug for AsyncSmtpTransport<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("AsyncSmtpTransport");
builder.field("inner", &self.inner);
builder.finish()
} }
} }
@@ -165,6 +285,9 @@ where
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
#[cfg(feature = "pool")]
inner: Arc::clone(&self.inner),
#[cfg(not(feature = "pool"))]
inner: self.inner.clone(), inner: self.inner.clone(),
} }
} }
@@ -172,14 +295,30 @@ where
/// Contains client configuration. /// Contains client configuration.
/// Instances of this struct can be created using functions of [`AsyncSmtpTransport`]. /// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
#[allow(missing_debug_implementations)] #[derive(Debug, Clone)]
#[derive(Clone)] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransportBuilder { pub struct AsyncSmtpTransportBuilder {
info: SmtpInfo, info: SmtpInfo,
#[cfg(feature = "pool")]
pool_config: PoolConfig,
} }
/// Builder for the SMTP `AsyncSmtpTransport` /// Builder for the SMTP `AsyncSmtpTransport`
impl AsyncSmtpTransportBuilder { impl AsyncSmtpTransportBuilder {
// Create new builder with default parameters
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
let info = SmtpInfo {
server: server.into(),
..Default::default()
};
AsyncSmtpTransportBuilder {
info,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Set the name used during EHLO /// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self { pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name; self.info.hello_name = name;
@@ -204,21 +343,43 @@ impl AsyncSmtpTransportBuilder {
self self
} }
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
self.info.timeout = timeout;
self
}
/// Set the TLS settings to use /// Set the TLS settings to use
#[cfg(any( #[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls", feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
)))
)]
pub fn tls(mut self, tls: super::Tls) -> Self { pub fn tls(mut self, tls: super::Tls) -> Self {
self.info.tls = tls; self.info.tls = tls;
self self
} }
/// Build the transport (with default pool if enabled) /// Use a custom configuration for the connection pool
///
/// Defaults can be found at [`PoolConfig`]
#[cfg(feature = "pool")]
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
self.pool_config = pool_config;
self
}
/// Build the transport
pub fn build<E>(self) -> AsyncSmtpTransport<E> pub fn build<E>(self) -> AsyncSmtpTransport<E>
where where
E: Executor, E: Executor,
@@ -227,14 +388,18 @@ impl AsyncSmtpTransportBuilder {
info: self.info, info: self.info,
marker_: PhantomData, marker_: PhantomData,
}; };
#[cfg(feature = "pool")]
let client = Pool::new(self.pool_config, client);
AsyncSmtpTransport { inner: client } AsyncSmtpTransport { inner: client }
} }
} }
/// Build client /// Build client
pub struct AsyncSmtpClient<C> { pub struct AsyncSmtpClient<E> {
info: SmtpInfo, info: SmtpInfo,
marker_: PhantomData<C>, marker_: PhantomData<E>,
} }
impl<E> AsyncSmtpClient<E> impl<E> AsyncSmtpClient<E>
@@ -248,18 +413,29 @@ where
let mut conn = E::connect( let mut conn = E::connect(
&self.info.server, &self.info.server,
self.info.port, self.info.port,
self.info.timeout,
&self.info.hello_name, &self.info.hello_name,
&self.info.tls, &self.info.tls,
) )
.await?; .await?;
if let Some(credentials) = &self.info.credentials { if let Some(credentials) = &self.info.credentials {
conn.auth(&self.info.authentication, &credentials).await?; conn.auth(&self.info.authentication, credentials).await?;
} }
Ok(conn) Ok(conn)
} }
} }
impl<E> Debug for AsyncSmtpClient<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("AsyncSmtpClient");
builder.field("info", &self.info);
builder.finish()
}
}
// `clone` is unused when the `pool` feature is on
#[allow(dead_code)]
impl<E> AsyncSmtpClient<E> impl<E> AsyncSmtpClient<E>
where where
E: Executor, E: Executor,
@@ -271,22 +447,3 @@ where
} }
} }
} }
#[doc(hidden)]
#[deprecated(note = "use lettre::Executor instead")]
pub use crate::Executor as AsyncSmtpConnector;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio02Executor instead")]
#[cfg(feature = "tokio02")]
pub type Tokio02Connector = crate::Tokio02Executor;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio1Executor instead")]
#[cfg(feature = "tokio1")]
pub type Tokio1Connector = crate::Tokio1Executor;
#[doc(hidden)]
#[deprecated(note = "use lettre::AsyncStd1Executor instead")]
#[cfg(feature = "async-std1")]
pub type AsyncStd1Connector = crate::AsyncStd1Executor;

View File

@@ -1,14 +1,16 @@
//! Provides limited SASL authentication mechanisms //! Provides limited SASL authentication mechanisms
use crate::transport::smtp::error::Error; use std::fmt::{self, Debug, Display, Formatter};
use std::fmt::{self, Display, Formatter};
use crate::transport::smtp::error::{self, Error};
/// Accepted authentication mechanisms /// Accepted authentication mechanisms
///
/// Trying LOGIN last as it is deprecated. /// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login]; pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Contains user credentials /// Contains user credentials
#[derive(PartialEq, Eq, Clone, Hash, Debug)] #[derive(PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Credentials { pub struct Credentials {
authentication_identity: String, authentication_identity: String,
@@ -35,6 +37,12 @@ where
} }
} }
impl Debug for Credentials {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Credentials").finish()
}
}
/// Represents authentication mechanisms /// Represents authentication mechanisms
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)] #[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -43,7 +51,7 @@ pub enum Mechanism {
/// [RFC 4616](https://tools.ietf.org/html/rfc4616) /// [RFC 4616](https://tools.ietf.org/html/rfc4616)
Plain, Plain,
/// LOGIN authentication mechanism /// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like office365) /// Obsolete but needed for some providers (like Office 365)
/// ///
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt). /// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
Login, Login,
@@ -63,7 +71,7 @@ impl Display for Mechanism {
} }
impl Mechanism { impl Mechanism {
/// Does the mechanism supports initial response /// Does the mechanism support initial response?
pub fn supports_initial_response(self) -> bool { pub fn supports_initial_response(self) -> bool {
match self { match self {
Mechanism::Plain | Mechanism::Xoauth2 => true, Mechanism::Plain | Mechanism::Xoauth2 => true,
@@ -80,28 +88,30 @@ impl Mechanism {
) -> Result<String, Error> { ) -> Result<String, Error> {
match self { match self {
Mechanism::Plain => match challenge { Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")), Some(_) => Err(error::client("This mechanism does not expect a challenge")),
None => Ok(format!( None => Ok(format!(
"\u{0}{}\u{0}{}", "\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret credentials.authentication_identity, credentials.secret
)), )),
}, },
Mechanism::Login => { Mechanism::Login => {
let decoded_challenge = let decoded_challenge = challenge
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?; .ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) { if ["User Name", "Username:", "Username", "User Name\0"]
return Ok(credentials.authentication_identity.to_string()); .contains(&decoded_challenge)
{
return Ok(credentials.authentication_identity.clone());
} }
if vec!["Password", "Password:"].contains(&decoded_challenge) { if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
return Ok(credentials.secret.to_string()); return Ok(credentials.secret.clone());
} }
Err(Error::Client("Unrecognized challenge")) Err(error::client("Unrecognized challenge"))
} }
Mechanism::Xoauth2 => match challenge { Mechanism::Xoauth2 => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")), Some(_) => Err(error::client("This mechanism does not expect a challenge")),
None => Ok(format!( None => Ok(format!(
"user={}\x01auth=Bearer {}\x01\x01", "user={}\x01auth=Bearer {}\x01\x01",
credentials.authentication_identity, credentials.secret credentials.authentication_identity, credentials.secret
@@ -119,7 +129,7 @@ mod test {
fn test_plain() { fn test_plain() {
let mechanism = Mechanism::Plain; let mechanism = Mechanism::Plain;
let credentials = Credentials::new("username".to_string(), "password".to_string()); let credentials = Credentials::new("username".to_owned(), "password".to_owned());
assert_eq!( assert_eq!(
mechanism.response(&credentials, None).unwrap(), mechanism.response(&credentials, None).unwrap(),
@@ -132,7 +142,7 @@ mod test {
fn test_login() { fn test_login() {
let mechanism = Mechanism::Login; let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string()); let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
assert_eq!( assert_eq!(
mechanism.response(&credentials, Some("Username")).unwrap(), mechanism.response(&credentials, Some("Username")).unwrap(),
@@ -150,8 +160,8 @@ mod test {
let mechanism = Mechanism::Xoauth2; let mechanism = Mechanism::Xoauth2;
let credentials = Credentials::new( let credentials = Credentials::new(
"username".to_string(), "username".to_owned(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(), "vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
); );
assert_eq!( assert_eq!(
@@ -164,7 +174,7 @@ mod test {
#[test] #[test]
fn test_from_user_pass_for_credentials() { fn test_from_user_pass_for_credentials() {
assert_eq!( assert_eq!(
Credentials::new("alice".to_string(), "wonderland".to_string()), Credentials::new("alice".to_owned(), "wonderland".to_owned()),
Credentials::from(("alice", "wonderland")) Credentials::from(("alice", "wonderland"))
); );
} }

View File

@@ -1,12 +1,17 @@
use std::{fmt::Display, io}; use std::{fmt::Display, net::IpAddr, time::Duration};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use super::{AsyncNetworkStream, ClientCodec, TlsParameters}; #[cfg(feature = "tokio1")]
use super::async_net::AsyncTokioStream;
#[cfg(feature = "tracing")]
use super::escape_crlf;
use super::{AsyncNetworkStream, ClientCodec, ConnectionState, TlsParameters};
use crate::{ use crate::{
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
commands::*, commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
error,
error::Error, error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}, extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response}, response::{parse_response, Response},
@@ -14,62 +19,75 @@ use crate::{
Envelope, Envelope,
}; };
#[cfg(feature = "tracing")]
use super::escape_crlf;
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 /// Structure that implements the SMTP client
pub struct AsyncSmtpConnection { pub struct AsyncSmtpConnection {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
stream: BufReader<AsyncNetworkStream>, stream: BufReader<AsyncNetworkStream>,
/// Panic state
panic: bool,
/// Information about the server /// Information about the server
server_info: ServerInfo, server_info: ServerInfo,
} }
impl AsyncSmtpConnection { impl AsyncSmtpConnection {
/// Get information about the server
pub fn server_info(&self) -> &ServerInfo { pub fn server_info(&self) -> &ServerInfo {
&self.server_info &self.server_info
} }
/// Connects to the configured server /// Connects with existing async stream
/// ///
/// Sends EHLO and parses server information /// Sends EHLO and parses server information
#[cfg(feature = "tokio02")] #[cfg(feature = "tokio1")]
pub async fn connect_tokio02( pub async fn connect_with_transport(
hostname: &str, stream: Box<dyn AsyncTokioStream>,
port: u16,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio02(hostname, port, tls_parameters).await?; let stream = AsyncNetworkStream::use_existing_tokio1(stream);
Self::connect_impl(stream, hello_name).await Self::connect_impl(stream, hello_name).await
} }
/// Connects to the configured server /// Connects to the configured server
/// ///
/// If `tls_parameters` is `Some`, then the connection will use Implicit TLS (sometimes
/// referred to as `SMTPS`). See also [`AsyncSmtpConnection::starttls`].
///
/// If `local_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 /// Sends EHLO and parses server information
///
/// # Example
///
/// ```no_run
/// # use std::time::Duration;
/// # use lettre::transport::smtp::{client::{AsyncSmtpConnection, TlsParameters}, extension::ClientId};
/// # use tokio1_crate::{self as tokio, net::ToSocketAddrs as _};
/// #
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let connection = AsyncSmtpConnection::connect_tokio1(
/// ("example.com", 465),
/// Some(Duration::from_secs(60)),
/// &ClientId::default(),
/// Some(TlsParameters::new("example.com".to_owned())?),
/// None,
/// )
/// .await
/// .unwrap();
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub async fn connect_tokio1( pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
hostname: &str, server: T,
port: u16, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
local_address: Option<IpAddr>,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio1(hostname, port, tls_parameters).await?; let stream =
AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address)
.await?;
Self::connect_impl(stream, hello_name).await Self::connect_impl(stream, hello_name).await
} }
@@ -77,13 +95,13 @@ impl AsyncSmtpConnection {
/// ///
/// Sends EHLO and parses server information /// Sends EHLO and parses server information
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
pub async fn connect_asyncstd1( pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
hostname: &str, server: T,
port: u16, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_asyncstd1(hostname, port, tls_parameters).await?; let stream = AsyncNetworkStream::connect_asyncstd1(server, timeout, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await Self::connect_impl(stream, hello_name).await
} }
@@ -94,7 +112,6 @@ impl AsyncSmtpConnection {
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = AsyncSmtpConnection { let mut conn = AsyncSmtpConnection {
stream, stream,
panic: false,
server_info: ServerInfo::default(), server_info: ServerInfo::default(),
}; };
// TODO log // TODO log
@@ -121,91 +138,95 @@ impl AsyncSmtpConnection {
if envelope.has_non_ascii_addresses() { if envelope.has_non_ascii_addresses() {
if !self.server_info().supports_feature(Extension::SmtpUtfEight) { if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
// don't try to send non-ascii addresses (per RFC) // don't try to send non-ascii addresses (per RFC)
return Err(Error::Client( return Err(error::client(
"Envelope contains non-ascii chars but server does not support SMTPUTF8", "Envelope contains non-ascii chars but server does not support SMTPUTF8",
)); ));
} }
mail_options.push(MailParameter::SmtpUtfEight); mail_options.push(MailParameter::SmtpUtfEight);
} }
// Check for non-ascii content in message // Check for non-ascii content in the message
if !email.is_ascii() { if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) { if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(Error::Client( return Err(error::client(
"Message contains non-ascii chars but server does not support 8BITMIME", "Message contains non-ascii chars but server does not support 8BITMIME",
)); ));
} }
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options)) self.command(Mail::new(envelope.from().cloned(), mail_options))
.await, .await?;
self
);
// Recipient // Recipient
for to_address in envelope.to() { for to_address in envelope.to() {
try_smtp!( self.command(Rcpt::new(to_address.clone(), vec![])).await?;
self.command(Rcpt::new(to_address.clone(), vec![])).await,
self
);
} }
// Data // Data
try_smtp!(self.command(Data).await, self); self.command(Data).await?;
// Message content // Message content
let result = try_smtp!(self.message(email).await, self); self.message(email).await
Ok(result)
} }
pub fn has_broken(&self) -> bool { 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 { pub fn can_starttls(&self) -> bool {
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls) !self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
} }
/// Upgrade the connection using `STARTTLS`.
///
/// As described in [rfc3207]. Note that this mechanism has been deprecated in [rfc8314].
///
/// [rfc3207]: https://www.rfc-editor.org/rfc/rfc3207
/// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314
#[allow(unused_variables)] #[allow(unused_variables)]
pub async fn starttls( pub async fn starttls(
&mut self, mut self,
tls_parameters: TlsParameters, tls_parameters: TlsParameters,
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<(), Error> { ) -> Result<Self, Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
try_smtp!(self.command(Starttls).await, self); self.command(Starttls).await?;
try_smtp!( let stream = self.stream.into_inner();
self.stream.get_mut().upgrade_tls(tls_parameters).await, let stream = stream.upgrade_tls(tls_parameters).await?;
self self.stream = BufReader::new(stream);
);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
try_smtp!(self.ehlo(hello_name).await, self); self.ehlo(hello_name).await?;
Ok(()) Ok(self)
} else { } else {
Err(Error::Client("STARTTLS is not supported on this server")) Err(error::client("STARTTLS is not supported on this server"))
} }
} }
/// Send EHLO and update server info /// Send EHLO and update server info
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> { async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self); let ehlo_response = self.command(Ehlo::new(hello_name.clone())).await?;
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self); self.server_info = ServerInfo::from_response(&ehlo_response)?;
Ok(()) Ok(())
} }
pub async fn quit(&mut self) -> Result<Response, Error> { 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) { pub async fn abort(&mut self) {
// Only try to quit if we are not already broken match self.stream.get_ref().state() {
if !self.panic { ConnectionState::Ok | ConnectionState::Broken => {
self.panic = true;
let _ = self.command(Quit).await; let _ = self.command(Quit).await;
let _ = self.stream.close().await;
self.stream.get_mut().set_state(ConnectionState::Closed);
}
ConnectionState::Closed => {}
} }
} }
@@ -224,7 +245,7 @@ impl AsyncSmtpConnection {
self.command(Noop).await.is_ok() self.command(Noop).await.is_ok()
} }
/// Sends an AUTH command with the given mechanism, and handles challenge if needed /// Sends an AUTH command with the given mechanism, and handles the challenge if needed
pub async fn auth( pub async fn auth(
&mut self, &mut self,
mechanisms: &[Mechanism], mechanisms: &[Mechanism],
@@ -233,31 +254,27 @@ impl AsyncSmtpConnection {
let mechanism = self let mechanism = self
.server_info .server_info
.get_auth_mechanism(mechanisms) .get_auth_mechanism(mechanisms)
.ok_or(Error::Client( .ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
"No compatible authentication mechanism was found",
))?;
// Limit challenges to avoid blocking // Limit challenges to avoid blocking
let mut challenges = 10; let mut challenges: u8 = 10;
let mut response = self let mut response = self
.command(Auth::new(mechanism, credentials.clone(), None)?) .command(Auth::new(mechanism, credentials.clone(), None)?)
.await?; .await?;
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = try_smtp!( response = self
self.command(Auth::new_from_response( .command(Auth::new_from_response(
mechanism, mechanism,
credentials.clone(), credentials.clone(),
&response, &response,
)?) )?)
.await, .await?;
self
);
} }
if challenges == 0 { if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges")) Err(error::response("Unexpected number of challenges"))
} else { } else {
Ok(response) Ok(response)
} }
@@ -281,8 +298,21 @@ impl AsyncSmtpConnection {
/// Writes a string to the server /// Writes a string to the server
async fn write(&mut self, string: &[u8]) -> Result<(), Error> { async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_mut().write_all(string).await?; self.stream.get_ref().state().verify()?;
self.stream.get_mut().flush().await?; self.stream.get_mut().set_state(ConnectionState::Broken);
self.stream
.get_mut()
.write_all(string)
.await
.map_err(error::network)?;
self.stream
.get_mut()
.flush()
.await
.map_err(error::network)?;
self.stream.get_mut().set_state(ConnectionState::Ok);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string))); tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
@@ -291,29 +321,49 @@ impl AsyncSmtpConnection {
/// Gets the SMTP response /// Gets the SMTP response
pub async fn read_response(&mut self) -> Result<Response, Error> { 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); let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer).await? > 0 { while self
.stream
.read_line(&mut buffer)
.await
.map_err(error::network)?
> 0
{
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("<< {}", escape_crlf(&buffer)); tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) { match parse_response(&buffer) {
Ok((_remaining, response)) => { Ok((_remaining, response)) => {
if response.is_positive() { self.stream.get_mut().set_state(ConnectionState::Ok);
return Ok(response);
}
return Err(response.into()); return if response.is_positive() {
Ok(response)
} else {
Err(error::code(
response.code(),
Some(response.message().collect()),
))
};
} }
Err(nom::Err::Failure(e)) => { Err(nom::Err::Failure(e)) => {
return Err(Error::Parsing(e.code)); return Err(error::response(e.to_string()));
} }
Err(nom::Err::Incomplete(_)) => { /* read more */ } Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => { Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.code)); return Err(error::response(e.to_string()));
} }
} }
} }
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into()) Err(error::response("incomplete response"))
}
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
} }
} }

View File

@@ -1,60 +1,66 @@
#[cfg(any(
feature = "tokio02-rustls-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
))]
use std::sync::Arc;
use std::{ use std::{
net::SocketAddr, fmt, io, mem,
net::{IpAddr, SocketAddr},
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
time::Duration,
}; };
use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
Result as IoResult,
};
#[cfg(feature = "tokio02")]
use tokio02_crate::io::{AsyncRead as _, AsyncWrite as _};
#[cfg(feature = "tokio1")]
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
#[cfg(feature = "async-std1")]
use async_std::net::TcpStream as AsyncStd1TcpStream;
#[cfg(feature = "tokio02")]
use tokio02_crate::net::TcpStream as Tokio02TcpStream;
#[cfg(feature = "tokio1")]
use tokio1_crate::net::TcpStream as Tokio1TcpStream;
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream; use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "tokio02-native-tls")] #[cfg(feature = "async-std1")]
use tokio02_native_tls_crate::TlsStream as Tokio02TlsStream; use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
use futures_io::{
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")]
use tokio1_crate::io::{AsyncRead, AsyncWrite, ReadBuf as Tokio1ReadBuf};
#[cfg(feature = "tokio1")]
use tokio1_crate::net::{
TcpSocket as Tokio1TcpSocket, TcpStream as Tokio1TcpStream,
ToSocketAddrs as Tokio1ToSocketAddrs,
};
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream; use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "async-std1-rustls-tls")]
use async_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio02-rustls-tls")]
use tokio02_rustls::client::TlsStream as Tokio02RustlsTlsStream;
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream; use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
#[cfg(any( #[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls",
feature = "async-std1-native-tls", feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::TlsParameters; use super::{ConnectionState, TlsParameters};
use crate::transport::smtp::Error; #[cfg(feature = "tokio1")]
use crate::transport::smtp::client::net::resolved_address_filter;
use crate::transport::smtp::{error, Error};
/// A network stream /// A network stream
#[derive(Debug)]
pub struct AsyncNetworkStream { pub struct AsyncNetworkStream {
inner: InnerAsyncNetworkStream, inner: InnerAsyncNetworkStream,
state: ConnectionState,
}
#[cfg(feature = "tokio1")]
pub trait AsyncTokioStream: AsyncRead + AsyncWrite + Send + Sync + Unpin + fmt::Debug {
fn peer_addr(&self) -> io::Result<SocketAddr>;
}
#[cfg(feature = "tokio1")]
impl AsyncTokioStream for Tokio1TcpStream {
fn peer_addr(&self) -> io::Result<SocketAddr> {
self.peer_addr()
}
} }
/// Represents the different types of underlying network streams /// Represents the different types of underlying network streams
@@ -62,25 +68,20 @@ pub struct AsyncNetworkStream {
// so clippy::large_enum_variant doesn't make sense here // so clippy::large_enum_variant doesn't make sense here
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug)]
enum InnerAsyncNetworkStream { enum InnerAsyncNetworkStream {
/// Plain Tokio 0.2 TCP stream
#[cfg(feature = "tokio02")]
Tokio02Tcp(Tokio02TcpStream),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-native-tls")]
Tokio02NativeTls(Tokio02TlsStream<Tokio02TcpStream>),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-rustls-tls")]
Tokio02RustlsTls(Tokio02RustlsTlsStream<Tokio02TcpStream>),
/// Plain Tokio 1.x TCP stream /// Plain Tokio 1.x TCP stream
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
Tokio1Tcp(Tokio1TcpStream), Tokio1Tcp(Box<dyn AsyncTokioStream>),
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
Tokio1NativeTls(Tokio1TlsStream<Tokio1TcpStream>), Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>), Tokio1RustlsTls(Tokio1RustlsTlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-boring-tls")]
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
/// Plain Tokio 1.x TCP stream /// Plain Tokio 1.x TCP stream
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
AsyncStd1Tcp(AsyncStd1TcpStream), AsyncStd1Tcp(AsyncStd1TcpStream),
@@ -90,142 +91,196 @@ enum InnerAsyncNetworkStream {
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>), AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
/// Can't be built
None,
} }
impl AsyncNetworkStream { impl AsyncNetworkStream {
fn new(inner: InnerAsyncNetworkStream) -> Self { fn new(inner: InnerAsyncNetworkStream) -> Self {
if let InnerAsyncNetworkStream::None = inner { AsyncNetworkStream {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built"); 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 /// Returns peer's address
pub fn peer_addr(&self) -> IoResult<SocketAddr> { pub fn peer_addr(&self) -> IoResult<SocketAddr> {
match self.inner { match &self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().peer_addr()
}
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(), InnerAsyncNetworkStream::Tokio1Tcp(s) => s.peer_addr(),
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref s) => { InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
s.get_ref().get_ref().get_ref().peer_addr() s.get_ref().get_ref().get_ref().peer_addr()
} }
#[cfg(feature = "tokio1-rustls-tls")] #[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(s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(), InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
#[cfg(feature = "async-std1-native-tls")] #[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")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => { }
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built"); }
Err(IoError::new(
ErrorKind::Other, #[cfg(feature = "tokio1")]
"InnerAsyncNetworkStream::None must never be built", pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
}
#[cfg(feature = "tokio1")]
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>,
local_addr: Option<IpAddr>,
) -> Result<AsyncNetworkStream, Error> {
async fn try_connect<T: Tokio1ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
local_addr: Option<IpAddr>,
) -> Result<Tokio1TcpStream, Error> {
let addrs = tokio1_crate::net::lookup_host(server)
.await
.map_err(error::connection)?
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
let mut last_err = None;
for addr in addrs {
let socket = match addr.ip() {
IpAddr::V4(_) => Tokio1TcpSocket::new_v4(),
IpAddr::V6(_) => Tokio1TcpSocket::new_v6(),
}
.map_err(error::connection)?;
if let Some(local_addr) = local_addr {
socket
.bind(SocketAddr::new(local_addr, 0))
.map_err(error::connection)?;
}
let connect_future = socket.connect(addr);
if let Some(timeout) = timeout {
match tokio1_crate::time::timeout(timeout, connect_future).await {
Ok(Ok(stream)) => return Ok(stream),
Ok(Err(err)) => last_err = Some(err),
Err(_) => {
last_err = Some(io::Error::new(
io::ErrorKind::TimedOut,
"connection timed out",
))
}
}
} else {
match connect_future.await {
Ok(stream) => return Ok(stream),
Err(err) => last_err = Some(err),
}
}
}
Err(match last_err {
Some(last_err) => error::connection(last_err),
None => error::connection("could not resolve to any supported address"),
})
}
let tcp_stream = try_connect(server, timeout, local_addr).await?;
let mut stream =
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
if let Some(tls_parameters) = tls_parameters {
stream = stream.upgrade_tls(tls_parameters).await?;
}
Ok(stream)
}
#[cfg(feature = "async-std1")]
pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
server: T,
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.
// Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have
// been connected, which is a blocking operation.
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
server: T,
timeout: Duration,
) -> Result<AsyncStd1TcpStream, Error> {
let addrs = server.to_socket_addrs().await.map_err(error::connection)?;
let mut last_err = None;
for addr in addrs {
let connect_future = AsyncStd1TcpStream::connect(&addr);
match async_std::future::timeout(timeout, connect_future).await {
Ok(Ok(stream)) => return Ok(stream),
Ok(Err(err)) => last_err = Some(err),
Err(_) => {
last_err = Some(io::Error::new(
io::ErrorKind::TimedOut,
"connection timed out",
)) ))
} }
} }
} }
#[cfg(feature = "tokio02")] Err(match last_err {
pub async fn connect_tokio02( Some(last_err) => error::connection(last_err),
hostname: &str, None => error::connection("could not resolve to any address"),
port: u16, })
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio02TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?;
}
Ok(stream)
} }
#[cfg(feature = "tokio1")] let tcp_stream = match timeout {
pub async fn connect_tokio1( Some(t) => try_connect_timeout(server, t).await?,
hostname: &str, None => AsyncStd1TcpStream::connect(server)
port: u16, .await
tls_parameters: Option<TlsParameters>, .map_err(error::connection)?,
) -> Result<AsyncNetworkStream, Error> { };
let tcp_stream = Tokio1TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?;
}
Ok(stream)
}
#[cfg(feature = "async-std1")]
pub async fn connect_asyncstd1(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream)); let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?; stream = stream.upgrade_tls(tls_parameters).await?;
} }
Ok(stream) Ok(stream)
} }
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> { pub async fn upgrade_tls(self, tls_parameters: TlsParameters) -> Result<Self, Error> {
match &self.inner { match self.inner {
#[cfg(all(
feature = "tokio02",
not(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))
))]
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio02-native-tls or the tokio02-rustls-tls feature");
}
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters).await?;
Ok(())
}
#[cfg(all( #[cfg(all(
feature = "tokio1", feature = "tokio1",
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls")) not(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls"
))
))] ))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => { InnerAsyncNetworkStream::Tokio1Tcp(_) => {
let _ = tls_parameters; let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature"); panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
} }
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(
InnerAsyncNetworkStream::Tokio1Tcp(_) => { feature = "tokio1-native-tls",
// get owned TcpStream feature = "tokio1-rustls-tls",
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None); feature = "tokio1-boring-tls"
let tcp_stream = match tcp_stream { ))]
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream, InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => {
_ => unreachable!(), let inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
}; .await
.map_err(error::connection)?;
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters).await?; Ok(Self {
Ok(()) inner,
state: ConnectionState::Ok,
})
} }
#[cfg(all( #[cfg(all(
feature = "async-std1", feature = "async-std1",
@@ -237,70 +292,30 @@ impl AsyncNetworkStream {
} }
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))] #[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => {
// get owned TcpStream let inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None); .await
let tcp_stream = match tcp_stream { .map_err(error::connection)?;
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream, Ok(Self {
_ => unreachable!(), inner,
}; state: ConnectionState::Ok,
})
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters).await?;
Ok(())
} }
_ => Ok(()), _ => Ok(self),
} }
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))] #[cfg(any(
async fn upgrade_tokio02_tls( feature = "tokio1-native-tls",
tcp_stream: Tokio02TcpStream, feature = "tokio1-rustls-tls",
mut tls_parameters: TlsParameters, feature = "tokio1-boring-tls"
) -> Result<InnerAsyncNetworkStream, Error> { ))]
let domain = std::mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
#[cfg(not(feature = "tokio02-native-tls"))]
panic!("built without the tokio02-native-tls feature");
#[cfg(feature = "tokio02-native-tls")]
return {
use tokio02_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
};
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio02-rustls-tls"))]
panic!("built without the tokio02-rustls-tls feature");
#[cfg(feature = "tokio02-rustls-tls")]
return {
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(stream))
};
}
}
}
#[allow(unused_variables)]
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
async fn upgrade_tokio1_tls( async fn upgrade_tokio1_tls(
tcp_stream: Tokio1TcpStream, tcp_stream: Box<dyn AsyncTokioStream>,
mut tls_parameters: TlsParameters, tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> { ) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain); let domain = tls_parameters.domain().to_owned();
match tls_parameters.connector { match tls_parameters.connector {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
@@ -313,7 +328,10 @@ impl AsyncNetworkStream {
use tokio1_native_tls_crate::TlsConnector; use tokio1_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector); let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?; let stream = connector
.connect(&domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream)) Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
}; };
} }
@@ -324,25 +342,49 @@ impl AsyncNetworkStream {
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
return { return {
use tokio1_rustls::{webpki::DNSNameRef, TlsConnector}; use tokio1_rustls::TlsConnector;
let domain = DNSNameRef::try_from_ascii_str(&domain)?; let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(Arc::new(config)); let connector = TlsConnector::from(config);
let stream = connector.connect(domain, tcp_stream).await?; let stream = connector
.connect(domain.to_owned(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream)) Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
}; };
} }
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
#[cfg(not(feature = "tokio1-boring-tls"))]
panic!("built without the tokio1-boring-tls feature");
#[cfg(feature = "tokio1-boring-tls")]
return {
let mut config = connector.configure().map_err(error::connection)?;
config.set_verify_hostname(tls_parameters.accept_invalid_hostnames);
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1BoringTls(stream))
};
}
} }
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))] #[cfg(any(
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls",
feature = "async-std1-boring-tls"
))]
async fn upgrade_asyncstd1_tls( async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream, tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters, mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> { ) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain); let domain = mem::take(&mut tls_parameters.domain);
match tls_parameters.connector { match tls_parameters.connector {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
@@ -372,39 +414,90 @@ impl AsyncNetworkStream {
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
return { return {
use async_rustls::{webpki::DNSNameRef, TlsConnector}; use futures_rustls::TlsConnector;
let domain = DNSNameRef::try_from_ascii_str(&domain)?; let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(Arc::new(config)); let connector = TlsConnector::from(config);
let stream = connector.connect(domain, tcp_stream).await?; let stream = connector
.connect(domain.to_owned(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream)) Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
}; };
} }
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
panic!("boring-tls isn't supported with async-std yet.");
}
} }
} }
pub fn is_encrypted(&self) -> bool { pub fn is_encrypted(&self) -> bool {
match self.inner { match &self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(_) => false,
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(_) => true,
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(_) => true,
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => false, InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true, InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true, InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false, InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true, InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true, InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
InnerAsyncNetworkStream::None => false, }
}
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(stream) => Ok(stream
.get_ref()
.peer_certificate()
.map_err(error::tls)?
.unwrap()
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.first()
.unwrap()
.to_vec()),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
.peer_certificate()
.unwrap()
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(t) => panic!("Unsupported"),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.first()
.unwrap()
.to_vec()),
} }
} }
} }
@@ -415,15 +508,9 @@ impl FuturesAsyncRead for AsyncNetworkStream {
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &mut [u8], buf: &mut [u8],
) -> Poll<IoResult<usize>> { ) -> Poll<IoResult<usize>> {
match self.inner { match &mut self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => { InnerAsyncNetworkStream::Tokio1Tcp(s) => {
let mut b = Tokio1ReadBuf::new(buf); let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) { match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())), Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -432,7 +519,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
} }
} }
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => { InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
let mut b = Tokio1ReadBuf::new(buf); let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) { match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())), Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -441,7 +528,16 @@ impl FuturesAsyncRead for AsyncNetworkStream {
} }
} }
#[cfg(feature = "tokio1-rustls-tls")] #[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())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(s) => {
let mut b = Tokio1ReadBuf::new(buf); let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) { match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())), Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -450,19 +546,11 @@ impl FuturesAsyncRead for AsyncNetworkStream {
} }
} }
#[cfg(feature = "async-std1")] #[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")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => { InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_read(cx, buf),
Pin::new(s).poll_read(cx, buf)
}
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => { InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
Pin::new(s).poll_read(cx, buf)
}
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
} }
} }
} }
@@ -473,87 +561,61 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &[u8], buf: &[u8],
) -> Poll<IoResult<usize>> { ) -> Poll<IoResult<usize>> {
match self.inner { match &mut self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1")] #[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")] #[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")] #[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(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1")] #[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")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => { InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
Pin::new(s).poll_write(cx, buf)
}
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => { InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
Pin::new(s).poll_write(cx, buf)
}
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
} }
} }
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner { match &mut self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1")] #[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")] #[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")] #[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(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1")] #[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")] #[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")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
} }
} }
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner { self.state = ConnectionState::Closed;
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx), match &mut self.inner {
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1")] #[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")] #[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")] #[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(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "async-std1")] #[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")] #[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")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_close(cx), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
} }
} }
} }

View File

@@ -1,49 +1,36 @@
use std::{ use std::{
fmt::Display, fmt::Display,
io::{self, BufRead, BufReader, Write}, io::{self, BufRead, BufReader, Write},
net::ToSocketAddrs, net::{IpAddr, ToSocketAddrs},
time::Duration, time::Duration,
}; };
use super::{ClientCodec, NetworkStream, TlsParameters}; #[cfg(feature = "tracing")]
use super::escape_crlf;
use super::{ClientCodec, ConnectionState, NetworkStream, TlsParameters};
use crate::{ use crate::{
address::Envelope, address::Envelope,
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
commands::*, commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
error,
error::Error, error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}, extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response}, response::{parse_response, Response},
}, },
}; };
#[cfg(feature = "tracing")]
use super::escape_crlf;
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 /// Structure that implements the SMTP client
pub struct SmtpConnection { pub struct SmtpConnection {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
stream: BufReader<NetworkStream>, stream: BufReader<NetworkStream>,
/// Panic state
panic: bool,
/// Information about the server /// Information about the server
server_info: ServerInfo, server_info: ServerInfo,
} }
impl SmtpConnection { impl SmtpConnection {
/// Get information about the server
pub fn server_info(&self) -> &ServerInfo { pub fn server_info(&self) -> &ServerInfo {
&self.server_info &self.server_info
} }
@@ -58,15 +45,15 @@ impl SmtpConnection {
timeout: Option<Duration>, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<&TlsParameters>, tls_parameters: Option<&TlsParameters>,
local_address: Option<IpAddr>,
) -> Result<SmtpConnection, Error> { ) -> Result<SmtpConnection, Error> {
let stream = NetworkStream::connect(server, timeout, tls_parameters)?; let stream = NetworkStream::connect(server, timeout, tls_parameters, local_address)?;
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = SmtpConnection { let mut conn = SmtpConnection {
stream, stream,
panic: false,
server_info: ServerInfo::default(), server_info: ServerInfo::default(),
}; };
conn.set_timeout(timeout)?; conn.set_timeout(timeout).map_err(error::network)?;
// TODO log // TODO log
let _response = conn.read_response()?; let _response = conn.read_response()?;
@@ -91,43 +78,42 @@ impl SmtpConnection {
if envelope.has_non_ascii_addresses() { if envelope.has_non_ascii_addresses() {
if !self.server_info().supports_feature(Extension::SmtpUtfEight) { if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
// don't try to send non-ascii addresses (per RFC) // don't try to send non-ascii addresses (per RFC)
return Err(Error::Client( return Err(error::client(
"Envelope contains non-ascii chars but server does not support SMTPUTF8", "Envelope contains non-ascii chars but server does not support SMTPUTF8",
)); ));
} }
mail_options.push(MailParameter::SmtpUtfEight); mail_options.push(MailParameter::SmtpUtfEight);
} }
// Check for non-ascii content in message // Check for non-ascii content in the message
if !email.is_ascii() { if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) { if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(Error::Client( return Err(error::client(
"Message contains non-ascii chars but server does not support 8BITMIME", "Message contains non-ascii chars but server does not support 8BITMIME",
)); ));
} }
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
try_smtp!( self.command(Mail::new(envelope.from().cloned(), mail_options))?;
self.command(Mail::new(envelope.from().cloned(), mail_options)),
self
);
// Recipient // Recipient
for to_address in envelope.to() { 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 // Data
try_smtp!(self.command(Data), self); self.command(Data)?;
// Message content // Message content
let result = try_smtp!(self.message(email), self); self.message(email)
Ok(result)
} }
pub fn has_broken(&self) -> bool { 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 { pub fn can_starttls(&self) -> bool {
@@ -136,46 +122,55 @@ impl SmtpConnection {
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn starttls( pub fn starttls(
&mut self, mut self,
tls_parameters: &TlsParameters, tls_parameters: &TlsParameters,
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<(), Error> { ) -> Result<Self, Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
{ {
try_smtp!(self.command(Starttls), self); self.command(Starttls)?;
try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self); let stream = self.stream.into_inner();
let stream = stream.upgrade_tls(tls_parameters)?;
self.stream = BufReader::new(stream);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
try_smtp!(self.ehlo(hello_name), self); self.ehlo(hello_name)?;
Ok(()) Ok(self)
} }
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] #[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
// This should never happen as `Tls` can only be created // This should never happen as `Tls` can only be created
// when a TLS library is enabled // when a TLS library is enabled
unreachable!("TLS support required but not supported"); unreachable!("TLS support required but not supported");
} else { } else {
Err(Error::Client("STARTTLS is not supported on this server")) Err(error::client("STARTTLS is not supported on this server"))
} }
} }
/// Send EHLO and update server info /// Send EHLO and update server info
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> { fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self); let ehlo_response = self.command(Ehlo::new(hello_name.clone()))?;
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self); self.server_info = ServerInfo::from_response(&ehlo_response)?;
Ok(()) Ok(())
} }
pub fn quit(&mut self) -> Result<Response, Error> { pub fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit), self)) self.command(Quit)
} }
pub fn abort(&mut self) { pub fn abort(&mut self) {
// Only try to quit if we are not already broken match self.stream.get_ref().state() {
if !self.panic { ConnectionState::Ok | ConnectionState::Broken => {
self.panic = true;
let _ = self.command(Quit); let _ = self.command(Quit);
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
self.stream.get_mut().set_state(ConnectionState::Closed);
}
ConnectionState::Closed => {}
} }
} }
@@ -200,7 +195,7 @@ impl SmtpConnection {
self.command(Noop).is_ok() self.command(Noop).is_ok()
} }
/// Sends an AUTH command with the given mechanism, and handles challenge if needed /// Sends an AUTH command with the given mechanism, and handles the challenge if needed
pub fn auth( pub fn auth(
&mut self, &mut self,
mechanisms: &[Mechanism], mechanisms: &[Mechanism],
@@ -209,9 +204,7 @@ impl SmtpConnection {
let mechanism = self let mechanism = self
.server_info .server_info
.get_auth_mechanism(mechanisms) .get_auth_mechanism(mechanisms)
.ok_or(Error::Client( .ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
"No compatible authentication mechanism was found",
))?;
// Limit challenges to avoid blocking // Limit challenges to avoid blocking
let mut challenges = 10; let mut challenges = 10;
@@ -219,18 +212,15 @@ impl SmtpConnection {
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = try_smtp!( response = self.command(Auth::new_from_response(
self.command(Auth::new_from_response(
mechanism, mechanism,
credentials.clone(), credentials.clone(),
&response, &response,
)?), )?)?;
self
);
} }
if challenges == 0 { if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges")) Err(error::response("Unexpected number of challenges"))
} else { } else {
Ok(response) Ok(response)
} }
@@ -238,11 +228,12 @@ impl SmtpConnection {
/// Sends the message content /// Sends the message content
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> { pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new(); let mut codec = ClientCodec::new();
let mut out_buf = Vec::with_capacity(message.len());
codec.encode(message, &mut out_buf); codec.encode(message, &mut out_buf);
self.write(out_buf.as_slice())?; self.write(out_buf.as_slice())?;
self.write(b"\r\n.\r\n")?; self.write(b"\r\n.\r\n")?;
self.read_response() self.read_response()
} }
@@ -254,8 +245,16 @@ impl SmtpConnection {
/// Writes a string to the server /// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> { fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_mut().write_all(string)?; self.stream.get_ref().state().verify()?;
self.stream.get_mut().flush()?; 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")] #[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string))); tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
@@ -264,29 +263,43 @@ impl SmtpConnection {
/// Gets the SMTP response /// Gets the SMTP response
pub fn read_response(&mut self) -> Result<Response, Error> { 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); let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer)? > 0 { while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("<< {}", escape_crlf(&buffer)); tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) { match parse_response(&buffer) {
Ok((_remaining, response)) => { Ok((_remaining, response)) => {
if response.is_positive() { self.stream.get_mut().set_state(ConnectionState::Ok);
return Ok(response);
}
return Err(response.into()); return if response.is_positive() {
Ok(response)
} else {
Err(error::code(
response.code(),
Some(response.message().collect()),
))
};
} }
Err(nom::Err::Failure(e)) => { Err(nom::Err::Failure(e)) => {
return Err(Error::Parsing(e.code)); return Err(error::response(e.to_string()));
} }
Err(nom::Err::Incomplete(_)) => { /* read more */ } Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => { Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.code)); return Err(error::response(e.to_string()));
} }
} }
} }
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into()) Err(error::response("incomplete response"))
}
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
} }
} }

View File

@@ -1,122 +0,0 @@
#![allow(missing_docs)]
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
use std::{
io::{self, Cursor, Read, Write},
sync::{Arc, Mutex},
};
pub type MockCursor = Cursor<Vec<u8>>;
#[derive(Clone, Debug)]
pub struct MockStream {
reader: Arc<Mutex<MockCursor>>,
writer: Arc<Mutex<MockCursor>>,
}
impl Default for MockStream {
fn default() -> Self {
Self::new()
}
}
impl MockStream {
pub fn new() -> MockStream {
MockStream {
reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
}
pub fn with_vec(vec: Vec<u8>) -> MockStream {
MockStream {
reader: Arc::new(Mutex::new(MockCursor::new(vec))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
}
pub fn take_vec(&mut self) -> Vec<u8> {
let mut cursor = self.writer.lock().unwrap();
let vec = cursor.get_ref().to_vec();
cursor.set_position(0);
cursor.get_mut().clear();
vec
}
pub fn next_vec(&mut self, vec: &[u8]) {
let mut cursor = self.reader.lock().unwrap();
cursor.set_position(0);
cursor.get_mut().clear();
cursor.get_mut().extend_from_slice(vec);
}
pub fn swap(&mut self) {
let mut cur_write = self.writer.lock().unwrap();
let mut cur_read = self.reader.lock().unwrap();
let vec_write = cur_write.get_ref().to_vec();
let vec_read = cur_read.get_ref().to_vec();
cur_write.set_position(0);
cur_read.set_position(0);
cur_write.get_mut().clear();
cur_read.get_mut().clear();
// swap cursors
cur_read.get_mut().extend_from_slice(vec_write.as_slice());
cur_write.get_mut().extend_from_slice(vec_read.as_slice());
}
}
impl Write for MockStream {
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
self.writer.lock().unwrap().write(msg)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.lock().unwrap().flush()
}
}
impl Read for MockStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.lock().unwrap().read(buf)
}
}
#[cfg(test)]
mod test {
use super::MockStream;
use std::io::{Read, Write};
#[test]
fn write_take_test() {
let mut mock = MockStream::new();
// write to mock stream
mock.write_all(&[1, 2, 3]).unwrap();
assert_eq!(mock.take_vec(), vec![1, 2, 3]);
}
#[test]
fn read_with_vec_test() {
let mut mock = MockStream::with_vec(vec![4, 5]);
let mut vec = Vec::new();
mock.read_to_end(&mut vec).unwrap();
assert_eq!(vec, vec![4, 5]);
}
#[test]
fn clone_test() {
let mut mock = MockStream::new();
let mut cloned = mock.clone();
mock.write_all(&[6, 7]).unwrap();
assert_eq!(cloned.take_vec(), vec![6, 7]);
}
#[test]
fn swap_test() {
let mut mock = MockStream::new();
let mut vec = Vec::new();
mock.write_all(&[8, 9, 10]).unwrap();
mock.swap();
mock.read_to_end(&mut vec).unwrap();
assert_eq!(vec, vec![8, 9, 10]);
}
}

View File

@@ -7,16 +7,14 @@
//! //!
//! # #[cfg(feature = "smtp-transport")] //! # #[cfg(feature = "smtp-transport")]
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::transport::smtp::{SMTP_PORT, extension::ClientId, commands::*, client::SmtpConnection}; //! use lettre::transport::smtp::{
//! 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)?; //! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None, None)?;
//! client.command( //! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
//! Mail::new(Some("user@example.com".parse()?), vec![]) //! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
//! )?;
//! client.command(
//! Rcpt::new("user@example.org".parse()?, vec![])
//! )?;
//! client.command(Data)?; //! client.command(Data)?;
//! client.message("Test email".as_bytes())?; //! client.message("Test email".as_bytes())?;
//! client.command(Quit)?; //! client.command(Quit)?;
@@ -27,75 +25,100 @@
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use std::fmt::Debug; use std::fmt::Debug;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub(crate) use self::async_connection::AsyncSmtpConnection; pub use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub(crate) use self::async_net::AsyncNetworkStream; pub use self::async_net::AsyncNetworkStream;
#[cfg(feature = "tokio1")]
pub use self::async_net::AsyncTokioStream;
use self::net::NetworkStream; use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub(super) use self::tls::InnerTlsParameters; pub(super) use self::tls::InnerTlsParameters;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub use self::tls::TlsVersion;
pub use self::{ pub use self::{
connection::SmtpConnection, connection::SmtpConnection,
mock::MockStream, tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
}; };
use super::{error, Error};
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_connection; mod async_connection;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_net; mod async_net;
mod connection; mod connection;
mod mock;
mod net; mod net;
mod tls; 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 /// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)] #[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct ClientCodec { struct ClientCodec {
escape_count: u8, status: CodecStatus,
} }
impl ClientCodec { impl ClientCodec {
/// Creates a new client codec /// Creates a new client codec
pub fn new() -> Self { pub fn new() -> Self {
ClientCodec::default() Self {
status: CodecStatus::StartOfNewLine,
}
} }
/// Adds transparency /// Adds transparency
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) { fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
match frame.len() { for &b in frame {
0 => { buf.push(b);
match self.escape_count { match (b, self.status) {
0 => buf.extend_from_slice(b"\r\n.\r\n"), (b'\r', _) => {
1 => buf.extend_from_slice(b"\n.\r\n"), self.status = CodecStatus::StartingNewLine;
2 => buf.extend_from_slice(b".\r\n"),
_ => unreachable!(),
} }
self.escape_count = 0; (b'\n', CodecStatus::StartingNewLine) => {
self.status = CodecStatus::StartOfNewLine;
} }
_ => { (_, CodecStatus::StartingNewLine) => {
let mut start = 0; self.status = CodecStatus::MiddleOfLine;
for (idx, byte) in frame.iter().enumerate() {
match self.escape_count {
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
2 => self.escape_count = if *byte == b'.' { 3 } else { 0 },
_ => unreachable!(),
} }
if self.escape_count == 3 { (b'.', CodecStatus::StartOfNewLine) => {
self.escape_count = 0; self.status = CodecStatus::MiddleOfLine;
buf.extend_from_slice(&frame[start..idx]); buf.push(b'.');
buf.extend_from_slice(b".");
start = idx;
} }
(_, CodecStatus::StartOfNewLine) => {
self.status = CodecStatus::MiddleOfLine;
} }
buf.extend_from_slice(&frame[start..]); _ => {}
} }
} }
} }
} }
#[derive(Debug, Copy, Clone)]
#[allow(clippy::enum_variant_names)]
enum CodecStatus {
/// We are past the first character of the current line
MiddleOfLine,
/// We just read a `\r` character
StartingNewLine,
/// We are at the start of a new line
StartOfNewLine,
}
/// Returns the string replacing all the CRLF with "\<CRLF\>" /// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays /// Used for debug displays
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@@ -109,10 +132,12 @@ mod test {
#[test] #[test]
fn test_codec() { fn test_codec() {
let mut buf = Vec::new();
let mut codec = ClientCodec::new(); let mut codec = ClientCodec::new();
let mut buf: Vec<u8> = vec![];
codec.encode(b".\r\n", &mut buf);
codec.encode(b"test\r\n", &mut buf); codec.encode(b"test\r\n", &mut buf);
codec.encode(b"test\r\n\r\n", &mut buf);
codec.encode(b".\r\n", &mut buf); codec.encode(b".\r\n", &mut buf);
codec.encode(b"\r\ntest", &mut buf); codec.encode(b"\r\ntest", &mut buf);
codec.encode(b"te\r\n.\r\nst", &mut buf); codec.encode(b"te\r\n.\r\nst", &mut buf);
@@ -121,9 +146,13 @@ mod test {
codec.encode(b"test\n", &mut buf); codec.encode(b"test\n", &mut buf);
codec.encode(b".test\n", &mut buf); codec.encode(b".test\n", &mut buf);
codec.encode(b"test", &mut buf); codec.encode(b"test", &mut buf);
codec.encode(b"test", &mut buf);
codec.encode(b"test\r\n", &mut buf);
codec.encode(b".test\r\n", &mut buf);
codec.encode(b"test.\r\n", &mut buf);
assert_eq!( assert_eq!(
String::from_utf8(buf).unwrap(), String::from_utf8(buf).unwrap(),
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest" "..\r\ntest\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntesttesttest\r\n..test\r\ntest.\r\n"
); );
} }

View File

@@ -2,24 +2,27 @@
use std::sync::Arc; use std::sync::Arc;
use std::{ use std::{
io::{self, Read, Write}, io::{self, Read, Write},
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs}, net::{IpAddr, Shutdown, SocketAddr, TcpStream, ToSocketAddrs},
time::Duration, time::Duration,
}; };
#[cfg(feature = "boring-tls")]
use boring::ssl::SslStream;
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::TlsStream; use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ClientSession, StreamOwned}; use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::{MockStream, TlsParameters}; use super::{ConnectionState, TlsParameters};
use crate::transport::smtp::Error; use crate::transport::smtp::{error, Error};
/// A network stream /// A network stream
pub struct NetworkStream { pub struct NetworkStream {
inner: InnerNetworkStream, inner: InnerNetworkStream,
state: ConnectionState,
} }
/// Represents the different types of underlying network streams /// Represents the different types of underlying network streams
@@ -34,44 +37,52 @@ enum InnerNetworkStream {
NativeTls(TlsStream<TcpStream>), NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream /// Encrypted TCP stream
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
RustlsTls(StreamOwned<ClientSession, TcpStream>), RustlsTls(StreamOwned<ClientConnection, TcpStream>),
/// Mock stream #[cfg(feature = "boring-tls")]
Mock(MockStream), BoringTls(SslStream<TcpStream>),
} }
impl NetworkStream { impl NetworkStream {
fn new(inner: InnerNetworkStream) -> Self { fn new(inner: InnerNetworkStream) -> Self {
NetworkStream { inner } NetworkStream {
inner,
state: ConnectionState::Ok,
}
} }
pub fn new_mock(mock: MockStream) -> Self { pub(super) fn state(&self) -> ConnectionState {
Self::new(InnerNetworkStream::Mock(mock)) self.state
}
pub(super) fn set_state(&mut self, state: ConnectionState) {
self.state = state;
} }
/// Returns peer's address /// Returns peer's address
pub fn peer_addr(&self) -> io::Result<SocketAddr> { pub fn peer_addr(&self) -> io::Result<SocketAddr> {
match self.inner { match &self.inner {
InnerNetworkStream::Tcp(ref s) => s.peer_addr(), InnerNetworkStream::Tcp(s) => s.peer_addr(),
#[cfg(feature = "native-tls")] #[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")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(), InnerNetworkStream::RustlsTls(s) => s.get_ref().peer_addr(),
InnerNetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new( #[cfg(feature = "boring-tls")]
Ipv4Addr::new(127, 0, 0, 1), InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
80,
))),
} }
} }
/// Shutdowns the connection /// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> { pub fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
match self.inner { self.state = ConnectionState::Closed;
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
match &self.inner {
InnerNetworkStream::Tcp(s) => s.shutdown(how),
#[cfg(feature = "native-tls")] #[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")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how), InnerNetworkStream::RustlsTls(s) => s.get_ref().shutdown(how),
InnerNetworkStream::Mock(_) => Ok(()), #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
} }
} }
@@ -79,58 +90,81 @@ impl NetworkStream {
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
tls_parameters: Option<&TlsParameters>, tls_parameters: Option<&TlsParameters>,
local_addr: Option<IpAddr>,
) -> Result<NetworkStream, Error> { ) -> Result<NetworkStream, Error> {
fn try_connect_timeout<T: ToSocketAddrs>( fn try_connect<T: ToSocketAddrs>(
server: T, server: T,
timeout: Duration, timeout: Option<Duration>,
local_addr: Option<IpAddr>,
) -> Result<TcpStream, Error> { ) -> Result<TcpStream, Error> {
let addrs = server.to_socket_addrs()?; let addrs = server
.to_socket_addrs()
.map_err(error::connection)?
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
let mut last_err = None;
for addr in addrs { for addr in addrs {
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) { let socket = socket2::Socket::new(
return Ok(result); Domain::for_address(addr),
Type::STREAM,
Some(Protocol::TCP),
)
.map_err(error::connection)?;
bind_local_address(&socket, &addr, local_addr)?;
if let Some(timeout) = timeout {
match socket.connect_timeout(&addr.into(), timeout) {
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
} else {
match socket.connect(&addr.into()) {
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
} }
} }
Err(Error::Client("Could not connect"))
} }
let tcp_stream = match timeout { Err(match last_err {
Some(t) => try_connect_timeout(server, t)?, Some(last_err) => error::connection(last_err),
None => TcpStream::connect(server)?, None => error::connection("could not resolve to any address"),
}; })
}
let tcp_stream = try_connect(server, timeout, local_addr)?;
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream)); let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters)?; stream = stream.upgrade_tls(tls_parameters)?;
} }
Ok(stream) Ok(stream)
} }
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> { pub fn upgrade_tls(self, tls_parameters: &TlsParameters) -> Result<Self, Error> {
match &self.inner { match self.inner {
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] #[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
InnerNetworkStream::Tcp(_) => { InnerNetworkStream::Tcp(_) => {
let _ = tls_parameters; let _ = tls_parameters;
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature"); panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
InnerNetworkStream::Tcp(_) => { InnerNetworkStream::Tcp(tcp_stream) => {
// get owned TcpStream let inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
let tcp_stream = Ok(Self {
std::mem::replace(&mut self.inner, InnerNetworkStream::Mock(MockStream::new())); inner,
let tcp_stream = match tcp_stream { state: ConnectionState::Ok,
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream, })
_ => unreachable!(),
};
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
Ok(())
} }
_ => Ok(()), _ => Ok(self),
} }
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
fn upgrade_tls_impl( fn upgrade_tls_impl(
tcp_stream: TcpStream, tcp_stream: TcpStream,
tls_parameters: &TlsParameters, tls_parameters: &TlsParameters,
@@ -140,101 +174,179 @@ impl NetworkStream {
InnerTlsParameters::NativeTls(connector) => { InnerTlsParameters::NativeTls(connector) => {
let stream = connector let stream = connector
.connect(tls_parameters.domain(), tcp_stream) .connect(tls_parameters.domain(), tcp_stream)
.map_err(|err| Error::Io(io::Error::new(io::ErrorKind::Other, err)))?; .map_err(error::connection)?;
InnerNetworkStream::NativeTls(stream) InnerNetworkStream::NativeTls(stream)
} }
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(connector) => { InnerTlsParameters::RustlsTls(connector) => {
use webpki::DNSNameRef; let domain = ServerName::try_from(tls_parameters.domain())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())?; let connection = ClientConnection::new(Arc::clone(connector), domain.to_owned())
let stream = StreamOwned::new( .map_err(error::connection)?;
ClientSession::new(&Arc::new(connector.clone()), domain), let stream = StreamOwned::new(connection, tcp_stream);
tcp_stream,
);
InnerNetworkStream::RustlsTls(stream) InnerNetworkStream::RustlsTls(stream)
} }
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
let stream = connector
.configure()
.map_err(error::connection)?
.verify_hostname(tls_parameters.accept_invalid_hostnames)
.connect(tls_parameters.domain(), tcp_stream)
.map_err(error::connection)?;
InnerNetworkStream::BoringTls(stream)
}
}) })
} }
pub fn is_encrypted(&self) -> bool { pub fn is_encrypted(&self) -> bool {
match self.inner { match &self.inner {
InnerNetworkStream::Tcp(_) | InnerNetworkStream::Mock(_) => false, InnerNetworkStream::Tcp(_) => false,
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => true, InnerNetworkStream::NativeTls(_) => true,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(_) => true, InnerNetworkStream::RustlsTls(_) => true,
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(_) => true,
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(stream) => Ok(stream
.peer_certificate()
.map_err(error::tls)?
.unwrap()
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(stream) => Ok(stream
.conn
.peer_certificates()
.unwrap()
.first()
.unwrap()
.to_vec()),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()
.peer_certificate()
.unwrap()
.to_der()
.map_err(error::tls)?),
} }
} }
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> { pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration), InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut stream) => { InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
stream.get_ref().set_read_timeout(duration)
}
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut stream) => { InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration),
stream.get_ref().set_read_timeout(duration) #[cfg(feature = "boring-tls")]
} InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
InnerNetworkStream::Mock(_) => Ok(()),
} }
} }
/// Set write timeout for IO calls /// Set write timeout for IO calls
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> { pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration), InnerNetworkStream::Tcp(stream) => stream.set_write_timeout(duration),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut stream) => { InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
stream.get_ref().set_write_timeout(duration)
}
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut stream) => { InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration),
stream.get_ref().set_write_timeout(duration) #[cfg(feature = "boring-tls")]
} InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
InnerNetworkStream::Mock(_) => Ok(()),
} }
} }
} }
impl Read for NetworkStream { impl Read for NetworkStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.read(buf), InnerNetworkStream::Tcp(s) => s.read(buf),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf), InnerNetworkStream::NativeTls(s) => s.read(buf),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf), InnerNetworkStream::RustlsTls(s) => s.read(buf),
InnerNetworkStream::Mock(ref mut s) => s.read(buf), #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.read(buf),
} }
} }
} }
impl Write for NetworkStream { impl Write for NetworkStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.write(buf), InnerNetworkStream::Tcp(s) => s.write(buf),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf), InnerNetworkStream::NativeTls(s) => s.write(buf),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf), InnerNetworkStream::RustlsTls(s) => s.write(buf),
InnerNetworkStream::Mock(ref mut s) => s.write(buf), #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.write(buf),
} }
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
match self.inner { match &mut self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.flush(), InnerNetworkStream::Tcp(s) => s.flush(),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.flush(), InnerNetworkStream::NativeTls(s) => s.flush(),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(), InnerNetworkStream::RustlsTls(s) => s.flush(),
InnerNetworkStream::Mock(ref mut s) => s.flush(), #[cfg(feature = "boring-tls")]
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 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(
socket: &socket2::Socket,
dst_addr: &SocketAddr,
local_addr: Option<IpAddr>,
) -> Result<(), Error> {
match local_addr {
Some(local_addr) => {
socket
.bind(&SocketAddr::new(local_addr, 0).into())
.map_err(error::connection)?;
}
_ => {
if cfg!(windows) {
// Windows requires a socket be bound before calling connect
let any: SocketAddr = match dst_addr {
SocketAddr::V4(_) => ([0, 0, 0, 0], 0).into(),
SocketAddr::V6(_) => ([0, 0, 0, 0, 0, 0, 0, 0], 0).into(),
};
socket.bind(&any.into()).map_err(error::connection)?;
}
}
}
Ok(())
}
/// When we have an iterator of resolved remote addresses, we must filter them to be the same
/// protocol as the local address binding. If no local address is set, then all will be matched.
pub(crate) fn resolved_address_filter(
resolved_addr: &SocketAddr,
local_addr: Option<IpAddr>,
) -> bool {
match local_addr {
Some(local_addr) => match resolved_addr.ip() {
IpAddr::V4(_) => local_addr.is_ipv4(),
IpAddr::V6(_) => local_addr.is_ipv6(),
},
None => true,
}
}

View File

@@ -1,20 +1,60 @@
use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use std::sync::Arc; use std::{io, sync::Arc};
#[cfg(feature = "boring-tls")]
use boring::{
ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder,
};
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError}; use rustls::{
#[cfg(feature = "rustls-tls")] client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
use webpki::DNSNameRef; crypto::{verify_tls12_signature, verify_tls13_signature},
pki_types::{CertificateDer, ServerName, UnixTime},
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
};
use crate::transport::smtp::error::Error; #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use crate::transport::smtp::{error, Error};
/// Accepted protocols by default. /// TLS protocol versions.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults. #[derive(Debug, Copy, Clone)]
// This is also rustls' default behavior #[non_exhaustive]
#[cfg(feature = "native-tls")] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12; pub enum TlsVersion {
/// TLS 1.0
///
/// Should only be used when trying to support legacy
/// SMTP servers that haven't updated to
/// at least TLS 1.2 yet.
///
/// Supported by `native-tls` and `boring-tls`.
Tlsv10,
/// TLS 1.1
///
/// Should only be used when trying to support legacy
/// SMTP servers that haven't updated to
/// at least TLS 1.2 yet.
///
/// Supported by `native-tls` and `boring-tls`.
Tlsv11,
/// TLS 1.2
///
/// A good option for most SMTP servers.
///
/// Supported by all TLS backends.
Tlsv12,
/// TLS 1.3
///
/// The most secure option, although not supported by all SMTP servers.
///
/// Although it is technically supported by all TLS backends,
/// trying to set it for `native-tls` will give a runtime error.
Tlsv13,
}
/// How to apply TLS to a client connection /// How to apply TLS to a client connection
#[derive(Clone)] #[derive(Clone)]
@@ -23,32 +63,86 @@ pub enum Tls {
/// Insecure connection only (for testing purposes) /// Insecure connection only (for testing purposes)
None, None,
/// Start with insecure connection and use `STARTTLS` when available /// Start with insecure connection and use `STARTTLS` when available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[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")))
)]
Opportunistic(TlsParameters), Opportunistic(TlsParameters),
/// Start with insecure connection and require `STARTTLS` /// Start with insecure connection and require `STARTTLS`
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[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")))
)]
Required(TlsParameters), Required(TlsParameters),
/// Use TLS wrapped connection /// Use TLS wrapped connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[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")))
)]
Wrapper(TlsParameters), Wrapper(TlsParameters),
} }
impl Debug for Tls {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Self::None => f.pad("None"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Opportunistic(_) => f.pad("Opportunistic"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Required(_) => f.pad("Required"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Wrapper(_) => f.pad("Wrapper"),
}
}
}
/// Source for the base set of root certificates to trust.
#[allow(missing_copy_implementations)]
#[derive(Clone, Debug, Default)]
pub enum CertificateStore {
/// Use the default for the TLS backend.
///
/// For native-tls, this will use the system certificate store on Windows, the keychain on
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`).
///
/// For rustls, this will also use the the system store if the `rustls-native-certs` feature is
/// enabled, or will fall back to `webpki-roots`.
///
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
#[default]
Default,
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
///
/// This option is only available in the rustls backend.
#[cfg(feature = "rustls-tls")]
WebpkiRoots,
/// Don't use any system certificates.
None,
}
/// Parameters to use for secure clients /// Parameters to use for secure clients
#[derive(Clone)] #[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct TlsParameters { pub struct TlsParameters {
pub(crate) connector: InnerTlsParameters, pub(crate) connector: InnerTlsParameters,
/// The domain name which is expected in the TLS certificate from the server /// The domain name which is expected in the TLS certificate from the server
pub(super) domain: String, pub(super) domain: String,
#[cfg(feature = "boring-tls")]
pub(super) accept_invalid_hostnames: bool,
} }
/// Builder for `TlsParameters` /// Builder for `TlsParameters`
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct TlsParametersBuilder { pub struct TlsParametersBuilder {
domain: String, domain: String,
cert_store: CertificateStore,
root_certs: Vec<Certificate>, root_certs: Vec<Certificate>,
accept_invalid_hostnames: bool, accept_invalid_hostnames: bool,
accept_invalid_certs: bool, accept_invalid_certs: bool,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
min_tls_version: TlsVersion,
} }
impl TlsParametersBuilder { impl TlsParametersBuilder {
@@ -56,12 +150,21 @@ impl TlsParametersBuilder {
pub fn new(domain: String) -> Self { pub fn new(domain: String) -> Self {
Self { Self {
domain, domain,
cert_store: CertificateStore::Default,
root_certs: Vec::new(), root_certs: Vec::new(),
accept_invalid_hostnames: false, accept_invalid_hostnames: false,
accept_invalid_certs: false, accept_invalid_certs: false,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
min_tls_version: TlsVersion::Tlsv12,
} }
} }
/// Set the source for the base set of root certificates to trust.
pub fn certificate_store(mut self, cert_store: CertificateStore) -> Self {
self.cert_store = cert_store;
self
}
/// Add a custom root certificate /// Add a custom root certificate
/// ///
/// Can be used to safely connect to a server using a self signed certificate, for example. /// Can be used to safely connect to a server using a self signed certificate, for example.
@@ -83,13 +186,22 @@ impl TlsParametersBuilder {
/// This method introduces significant vulnerabilities to man-in-the-middle attacks. /// This method introduces significant vulnerabilities to man-in-the-middle attacks.
/// ///
/// Hostname verification can only be disabled with the `native-tls` TLS backend. /// Hostname verification can only be disabled with the `native-tls` TLS backend.
#[cfg(feature = "native-tls")] #[cfg(any(feature = "native-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] #[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self { pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames; self.accept_invalid_hostnames = accept_invalid_hostnames;
self self
} }
/// Controls which minimum TLS version is allowed
///
/// Defaults to [`Tlsv12`][TlsVersion::Tlsv12].
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self {
self.min_tls_version = min_tls_version;
self
}
/// Controls whether invalid certificates are accepted /// Controls whether invalid certificates are accepted
/// ///
/// Defaults to `false`. /// Defaults to `false`.
@@ -111,22 +223,20 @@ impl TlsParametersBuilder {
self self
} }
/// Creates a new `TlsParameters` using native-tls or rustls /// Creates a new `TlsParameters` using native-tls, boring-tls or rustls
/// depending on which one is available /// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
// TODO: remove below line once native-tls is supported with async-std docsrs,
#[allow(unreachable_code)] doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn build(self) -> Result<TlsParameters, Error> { pub fn build(self) -> Result<TlsParameters, Error> {
// TODO: remove once native-tls is supported with async-std #[cfg(feature = "rustls-tls")]
#[cfg(all(feature = "rustls-tls", feature = "async-std1"))]
return self.build_rustls(); return self.build_rustls();
#[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
#[cfg(feature = "native-tls")]
return self.build_native(); return self.build_native();
#[cfg(all(not(feature = "rustls-tls"), feature = "boring-tls"))]
#[cfg(not(feature = "native-tls"))] return self.build_boring();
return self.build_rustls();
} }
/// Creates a new `TlsParameters` using native-tls with the provided configuration /// Creates a new `TlsParameters` using native-tls with the provided configuration
@@ -135,17 +245,93 @@ impl TlsParametersBuilder {
pub fn build_native(self) -> Result<TlsParameters, Error> { pub fn build_native(self) -> Result<TlsParameters, Error> {
let mut tls_builder = TlsConnector::builder(); let mut tls_builder = TlsConnector::builder();
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
tls_builder.disable_built_in_roots(true);
}
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in native tls"
)))
}
}
for cert in self.root_certs { for cert in self.root_certs {
tls_builder.add_root_certificate(cert.native_tls); tls_builder.add_root_certificate(cert.native_tls);
} }
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames); tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs); tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL)); let min_tls_version = match self.min_tls_version {
let connector = tls_builder.build()?; TlsVersion::Tlsv10 => Protocol::Tlsv10,
TlsVersion::Tlsv11 => Protocol::Tlsv11,
TlsVersion::Tlsv12 => Protocol::Tlsv12,
TlsVersion::Tlsv13 => {
return Err(error::tls(
"min tls version Tlsv13 not supported in native tls",
))
}
};
tls_builder.min_protocol_version(Some(min_tls_version));
let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector), connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain, domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
}
/// Creates a new `TlsParameters` using boring-tls with the provided configuration
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn build_boring(self) -> Result<TlsParameters, Error> {
use boring::ssl::{SslMethod, SslVerifyMode};
let mut tls_builder = SslConnector::builder(SslMethod::tls_client()).map_err(error::tls)?;
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 {
cert_store.add_cert(cert.boring_tls).map_err(error::tls)?;
}
}
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => SslVersion::TLS1,
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
TlsVersion::Tlsv12 => SslVersion::TLS1_2,
TlsVersion::Tlsv13 => SslVersion::TLS1_3,
};
tls_builder
.set_min_proto_version(Some(min_tls_version))
.map_err(error::tls)?;
let connector = tls_builder.build();
Ok(TlsParameters {
connector: InnerTlsParameters::BoringTls(connector),
domain: self.domain,
accept_invalid_hostnames: self.accept_invalid_hostnames,
}) })
} }
@@ -153,47 +339,98 @@ impl TlsParametersBuilder {
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> { pub fn build_rustls(self) -> Result<TlsParameters, Error> {
use webpki_roots::TLS_SERVER_ROOTS; let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match self.min_tls_version {
TlsVersion::Tlsv10 => {
return Err(error::tls("min tls version Tlsv10 not supported in rustls"))
}
TlsVersion::Tlsv11 => {
return Err(error::tls("min tls version Tlsv11 not supported in rustls"))
}
TlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
TlsVersion::Tlsv13 => just_version3,
};
let mut tls = ClientConfig::new(); let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
let tls = if self.accept_invalid_certs {
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 cert in self.root_certs {
for rustls_cert in cert.rustls { for rustls_cert in cert.rustls {
tls.root_store root_cert_store.add(rustls_cert).map_err(error::tls)?;
.add(&rustls_cert)
.map_err(|_| Error::InvalidCertificate)?;
} }
} }
if self.accept_invalid_certs {
tls.dangerous()
.set_certificate_verifier(Arc::new(InvalidCertsVerifier {}));
}
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS); tls.with_root_certificates(root_cert_store)
};
let tls = tls.with_no_client_auth();
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(tls), connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
domain: self.domain, domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
}) })
} }
} }
#[derive(Clone)] #[derive(Clone)]
#[allow(clippy::enum_variant_names)]
pub enum InnerTlsParameters { pub enum InnerTlsParameters {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
NativeTls(TlsConnector), NativeTls(TlsConnector),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
RustlsTls(ClientConfig), RustlsTls(Arc<ClientConfig>),
#[cfg(feature = "boring-tls")]
BoringTls(SslConnector),
} }
impl TlsParameters { impl TlsParameters {
/// Creates a new `TlsParameters` using native-tls or rustls /// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available /// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn new(domain: String) -> Result<Self, Error> { pub fn new(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build() TlsParametersBuilder::new(domain).build()
} }
/// Creates a new `TlsParameters` builder
pub fn builder(domain: String) -> TlsParametersBuilder { pub fn builder(domain: String) -> TlsParametersBuilder {
TlsParametersBuilder::new(domain) TlsParametersBuilder::new(domain)
} }
@@ -212,6 +449,13 @@ impl TlsParameters {
TlsParametersBuilder::new(domain).build_rustls() TlsParametersBuilder::new(domain).build_rustls()
} }
/// Creates a new `TlsParameters` using boring
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn new_boring(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_boring()
}
pub fn domain(&self) -> &str { pub fn domain(&self) -> &str {
&self.domain &self.domain
} }
@@ -224,37 +468,47 @@ pub struct Certificate {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate, native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>, rustls: Vec<CertificateDer<'static>>,
#[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509,
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
impl Certificate { impl Certificate {
/// Create a `Certificate` from a DER encoded certificate /// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> { pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
let native_tls_cert = let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
native_tls::Certificate::from_der(&der).map_err(|_| Error::InvalidCertificate)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_der(&der).map_err(error::tls)?;
Ok(Self { Ok(Self {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
native_tls: native_tls_cert, native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)], rustls: vec![der.into()],
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
}) })
} }
/// Create a `Certificate` from a PEM encoded certificate /// Create a `Certificate` from a PEM encoded certificate
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> { pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
let native_tls_cert = let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
native_tls::Certificate::from_pem(pem).map_err(|_| Error::InvalidCertificate)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
let rustls_cert = { let rustls_cert = {
use rustls::internal::pemfile;
use std::io::Cursor; use std::io::Cursor;
let mut pem = Cursor::new(pem); let mut pem = Cursor::new(pem);
pemfile::certs(&mut pem).map_err(|_| Error::InvalidCertificate)? rustls_pemfile::certs(&mut pem)
.collect::<io::Result<Vec<_>>>()
.map_err(|_| error::tls("invalid certificates"))?
}; };
Ok(Self { Ok(Self {
@@ -262,22 +516,66 @@ impl Certificate {
native_tls: native_tls_cert, native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: rustls_cert, rustls: rustls_cert,
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
}) })
} }
} }
impl Debug for Certificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Certificate").finish()
}
}
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
#[derive(Debug)]
struct InvalidCertsVerifier; struct InvalidCertsVerifier;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
impl ServerCertVerifier for InvalidCertsVerifier { impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert( fn verify_server_cert(
&self, &self,
_roots: &RootCertStore, _end_entity: &CertificateDer<'_>,
_presented_certs: &[rustls::Certificate], _intermediates: &[CertificateDer<'_>],
_dns_name: DNSNameRef<'_>, _server_name: &ServerName<'_>,
_ocsp_response: &[u8], _ocsp_response: &[u8],
) -> Result<ServerCertVerified, TLSError> { _now: UnixTime,
) -> Result<ServerCertVerified, TlsError> {
Ok(ServerCertVerified::assertion()) 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()
}
} }

View File

@@ -1,18 +1,19 @@
//! SMTP commands //! SMTP commands
use std::fmt::{self, Display, Formatter};
use crate::{ use crate::{
address::Address,
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
error::Error, error::{self, Error},
extension::{ClientId, MailParameter, RcptParameter}, extension::{ClientId, MailParameter, RcptParameter},
response::Response, response::Response,
}, },
Address,
}; };
use std::fmt::{self, Display, Formatter};
/// EHLO command /// EHLO command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ehlo { pub struct Ehlo {
client_id: ClientId, client_id: ClientId,
@@ -32,7 +33,7 @@ impl Ehlo {
} }
/// STARTTLS command /// STARTTLS command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Starttls; pub struct Starttls;
@@ -43,7 +44,7 @@ impl Display for Starttls {
} }
/// MAIL command /// MAIL command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Mail { pub struct Mail {
sender: Option<Address>, sender: Option<Address>,
@@ -55,10 +56,10 @@ impl Display for Mail {
write!( write!(
f, f,
"MAIL FROM:<{}>", "MAIL FROM:<{}>",
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("") self.sender.as_ref().map_or("", |s| s.as_ref())
)?; )?;
for parameter in &self.parameters { for parameter in &self.parameters {
write!(f, " {}", parameter)?; write!(f, " {parameter}")?;
} }
f.write_str("\r\n") f.write_str("\r\n")
} }
@@ -72,7 +73,7 @@ impl Mail {
} }
/// RCPT command /// RCPT command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rcpt { pub struct Rcpt {
recipient: Address, recipient: Address,
@@ -83,7 +84,7 @@ impl Display for Rcpt {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?; write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters { for parameter in &self.parameters {
write!(f, " {}", parameter)?; write!(f, " {parameter}")?;
} }
f.write_str("\r\n") f.write_str("\r\n")
} }
@@ -100,7 +101,7 @@ impl Rcpt {
} }
/// DATA command /// DATA command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Data; pub struct Data;
@@ -111,7 +112,7 @@ impl Display for Data {
} }
/// QUIT command /// QUIT command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Quit; pub struct Quit;
@@ -122,7 +123,7 @@ impl Display for Quit {
} }
/// NOOP command /// NOOP command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Noop; pub struct Noop;
@@ -133,7 +134,7 @@ impl Display for Noop {
} }
/// HELP command /// HELP command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Help { pub struct Help {
argument: Option<String>, argument: Option<String>,
@@ -143,7 +144,7 @@ impl Display for Help {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("HELP")?; f.write_str("HELP")?;
if let Some(argument) = &self.argument { if let Some(argument) = &self.argument {
write!(f, " {}", argument)?; write!(f, " {argument}")?;
} }
f.write_str("\r\n") f.write_str("\r\n")
} }
@@ -157,7 +158,7 @@ impl Help {
} }
/// VRFY command /// VRFY command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Vrfy { pub struct Vrfy {
argument: String, argument: String,
@@ -177,7 +178,7 @@ impl Vrfy {
} }
/// EXPN command /// EXPN command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Expn { pub struct Expn {
argument: String, argument: String,
@@ -197,7 +198,7 @@ impl Expn {
} }
/// RSET command /// RSET command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rset; pub struct Rset;
@@ -208,7 +209,7 @@ impl Display for Rset {
} }
/// AUTH command /// AUTH command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Auth { pub struct Auth {
mechanism: Mechanism, mechanism: Mechanism,
@@ -219,7 +220,7 @@ pub struct Auth {
impl Display for Auth { impl Display for Auth {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let encoded_response = self.response.as_ref().map(base64::encode); let encoded_response = self.response.as_ref().map(crate::base64::encode);
if self.mechanism.supports_initial_response() { if self.mechanism.supports_initial_response() {
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?; write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
@@ -261,16 +262,17 @@ impl Auth {
response: &Response, response: &Response,
) -> Result<Auth, Error> { ) -> Result<Auth, Error> {
if !response.has_code(334) { if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a challenge")); return Err(error::response("Expecting a challenge"));
} }
let encoded_challenge = response let encoded_challenge = response
.first_word() .first_word()
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?; .ok_or_else(|| error::response("Could not read auth challenge"))?;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("auth encoded challenge: {}", encoded_challenge); tracing::debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?; let decoded_base64 = crate::base64::decode(encoded_challenge).map_err(error::response)?;
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("auth decoded challenge: {}", decoded_challenge); tracing::debug!("auth decoded challenge: {}", decoded_challenge);
@@ -287,21 +289,22 @@ impl Auth {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::str::FromStr;
use super::*; use super::*;
use crate::transport::smtp::extension::MailBodyParameter; use crate::transport::smtp::extension::MailBodyParameter;
use std::str::FromStr;
#[test] #[test]
fn test_display() { fn test_display() {
let id = ClientId::Domain("localhost".to_string()); let id = ClientId::Domain("localhost".to_owned());
let email = Address::from_str("test@example.com").unwrap(); let email = Address::from_str("test@example.com").unwrap();
let mail_parameter = MailParameter::Other { let mail_parameter = MailParameter::Other {
keyword: "TEST".to_string(), keyword: "TEST".to_owned(),
value: Some("value".to_string()), value: Some("value".to_owned()),
}; };
let rcpt_parameter = RcptParameter::Other { let rcpt_parameter = RcptParameter::Other {
keyword: "TEST".to_string(), keyword: "TEST".to_owned(),
value: Some("value".to_string()), value: Some("value".to_owned()),
}; };
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n"); assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
assert_eq!( assert_eq!(
@@ -338,24 +341,18 @@ mod test {
format!("{}", Rcpt::new(email, vec![rcpt_parameter])), format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
"RCPT TO:<test@example.com> TEST=value\r\n" "RCPT TO:<test@example.com> TEST=value\r\n"
); );
assert_eq!(format!("{}", Quit), "QUIT\r\n"); assert_eq!(format!("{Quit}"), "QUIT\r\n");
assert_eq!(format!("{}", Data), "DATA\r\n"); assert_eq!(format!("{Data}"), "DATA\r\n");
assert_eq!(format!("{}", Noop), "NOOP\r\n"); assert_eq!(format!("{Noop}"), "NOOP\r\n");
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n"); assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
assert_eq!( assert_eq!(
format!("{}", Help::new(Some("test".to_string()))), format!("{}", Help::new(Some("test".to_owned()))),
"HELP test\r\n" "HELP test\r\n"
); );
assert_eq!( assert_eq!(format!("{}", Vrfy::new("test".to_owned())), "VRFY test\r\n");
format!("{}", Vrfy::new("test".to_string())), assert_eq!(format!("{}", Expn::new("test".to_owned())), "EXPN test\r\n");
"VRFY test\r\n" assert_eq!(format!("{Rset}"), "RSET\r\n");
); let credentials = Credentials::new("user".to_owned(), "password".to_owned());
assert_eq!(
format!("{}", Expn::new("test".to_string())),
"EXPN test\r\n"
);
assert_eq!(format!("{}", Rset), "RSET\r\n");
let credentials = Credentials::new("user".to_string(), "password".to_string());
assert_eq!( assert_eq!(
format!( format!(
"{}", "{}",

View 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)
}

View File

@@ -1,162 +1,191 @@
//! Error and result type for SMTP clients //! Error and result type for SMTP clients
use self::Error::*; use std::{error::Error as StdError, fmt};
use crate::transport::smtp::response::{Response, Severity};
use base64::DecodeError; use crate::{
use std::{ transport::smtp::response::{Code, Severity},
error::Error as StdError, BoxError,
fmt::{self, Display, Formatter},
io,
string::FromUtf8Error,
}; };
/// An enum of all error kinds. // Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
#[derive(Debug)]
pub enum Error { /// The Errors that may occur when sending an email over SMTP
/// Transient SMTP error, 4xx reply code pub struct Error {
/// inner: Box<Inner>,
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Response),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Response),
/// Error parsing a response
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsing(DecodeError),
/// Error parsing UTF8 in response
Utf8Parsing(FromUtf8Error),
/// Internal client error
Client(&'static str),
/// DNS resolution error
Resolution,
/// IO error
Io(io::Error),
/// TLS error
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
Tls(native_tls::Error),
/// Parsing error
Parsing(nom::error::ErrorKind),
/// Invalid hostname
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
InvalidDNSName(webpki::InvalidDNSNameError),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
InvalidCertificate,
#[cfg(feature = "r2d2")]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
Pool(r2d2::Error),
} }
impl Display for Error { struct Inner {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { kind: Kind,
match *self { source: Option<BoxError>,
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
Transient(ref err) => fmt.write_str(
err.first_line()
.unwrap_or("transient error during SMTP transaction"),
),
Permanent(ref err) => fmt.write_str(
err.first_line()
.unwrap_or("permanent error during SMTP transaction"),
),
ResponseParsing(err) => fmt.write_str(err),
ChallengeParsing(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Resolution => fmt.write_str("could not resolve hostname"),
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
#[cfg(feature = "native-tls")]
Tls(ref err) => err.fmt(fmt),
Parsing(ref err) => fmt.write_str(err.description()),
#[cfg(feature = "rustls-tls")]
InvalidDNSName(ref err) => err.fmt(fmt),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
InvalidCertificate => fmt.write_str("invalid certificate"),
#[cfg(feature = "r2d2")]
Pool(ref err) => err.fmt(fmt),
} }
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
}),
} }
} }
impl StdError for Error { /// Returns true if the error is from response
fn source(&self) -> Option<&(dyn StdError + 'static)> { pub fn is_response(&self) -> bool {
match *self { matches!(self.inner.kind, Kind::Response)
ChallengeParsing(ref err) => Some(&*err), }
Utf8Parsing(ref err) => Some(&*err),
Io(ref err) => Some(&*err), /// Returns true if the error is from client
#[cfg(feature = "native-tls")] pub fn is_client(&self) -> bool {
Tls(ref err) => Some(&*err), matches!(self.inner.kind, Kind::Client)
}
/// Returns true if the error is a transient SMTP error
pub fn is_transient(&self) -> bool {
matches!(self.inner.kind, Kind::Transient(_))
}
/// Returns true if the error is a permanent SMTP error
pub fn is_permanent(&self) -> bool {
matches!(self.inner.kind, Kind::Permanent(_))
}
/// Returns true if the error is caused by a timeout
pub fn is_timeout(&self) -> bool {
let mut source = self.source();
while let Some(err) = source {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
return io_err.kind() == std::io::ErrorKind::TimedOut;
}
source = err.source();
}
false
}
/// Returns true if the error is from TLS
#[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 is_tls(&self) -> bool {
matches!(self.inner.kind, Kind::Tls)
}
/// Returns the status code, if the error was generated from a response.
pub fn status(&self) -> Option<Code> {
match self.inner.kind {
Kind::Transient(code) | Kind::Permanent(code) => Some(code),
_ => None, _ => None,
} }
} }
} }
impl From<io::Error> for Error { #[derive(Debug)]
fn from(err: io::Error) -> Error { pub(crate) enum Kind {
Io(err) /// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Code),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Code),
/// Error parsing a response
Response,
/// Internal client error
Client,
/// Connection error
Connection,
/// Underlying network i/o error
Network,
/// TLS error
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("lettre::transport::smtp::Error");
builder.field("kind", &self.inner.kind);
if let Some(source) = &self.inner.source {
builder.field("source", source);
}
builder.finish()
} }
} }
#[cfg(feature = "native-tls")] impl fmt::Display for Error {
impl From<native_tls::Error> for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn from(err: native_tls::Error) -> Error { match &self.inner.kind {
Tls(err) 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(code) => {
write!(f, "transient error ({code})")?;
}
Kind::Permanent(code) => {
write!(f, "permanent error ({code})")?;
}
};
if let Some(e) = &self.inner.source {
write!(f, ": {e}")?;
}
Ok(())
} }
} }
impl From<nom::Err<nom::error::Error<&str>>> for Error { impl StdError for Error {
fn from(err: nom::Err<nom::error::Error<&str>>) -> Error { fn source(&self) -> Option<&(dyn StdError + 'static)> {
Parsing(match err { self.inner.source.as_ref().map(|e| {
nom::Err::Incomplete(_) => nom::error::ErrorKind::Complete, let r: &(dyn std::error::Error + 'static) = &**e;
nom::Err::Failure(e) => e.code, r
nom::Err::Error(e) => e.code,
}) })
} }
} }
impl From<DecodeError> for Error { pub(crate) fn code(c: Code, s: Option<String>) -> Error {
fn from(err: DecodeError) -> Error { match c.severity {
ChallengeParsing(err) Severity::TransientNegativeCompletion => Error::new(Kind::Transient(c), s),
Severity::PermanentNegativeCompletion => Error::new(Kind::Permanent(c), s),
_ => client("Unknown error code"),
} }
} }
impl From<FromUtf8Error> for Error { pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
fn from(err: FromUtf8Error) -> Error { Error::new(Kind::Response, Some(e))
Utf8Parsing(err)
}
} }
#[cfg(feature = "rustls-tls")] pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
impl From<webpki::InvalidDNSNameError> for Error { Error::new(Kind::Client, Some(e))
fn from(err: webpki::InvalidDNSNameError) -> Error {
InvalidDNSName(err)
}
} }
#[cfg(feature = "r2d2")] pub(crate) fn network<E: Into<BoxError>>(e: E) -> Error {
impl From<r2d2::Error> for Error { Error::new(Kind::Network, Some(e))
fn from(err: r2d2::Error) -> Error {
Pool(err)
}
} }
impl From<Response> for Error { pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
fn from(response: Response) -> Error { Error::new(Kind::Connection, Some(e))
match response.code.severity {
Severity::TransientNegativeCompletion => Transient(response),
Severity::PermanentNegativeCompletion => Permanent(response),
_ => Client("Unknown error code"),
}
}
} }
impl From<&'static str> for Error { #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
fn from(string: &'static str) -> Error { pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
Client(string) Error::new(Kind::Tls, Some(e))
}
} }

View File

@@ -1,13 +1,16 @@
//! ESMTP features //! ESMTP features
use crate::transport::smtp::{
authentication::Mechanism, error::Error, response::Response, util::XText,
};
use std::{ use std::{
collections::HashSet, collections::HashSet,
fmt::{self, Display, Formatter}, fmt::{self, Display, Formatter},
net::{Ipv4Addr, Ipv6Addr}, net::{Ipv4Addr, Ipv6Addr},
result::Result, };
use crate::transport::smtp::{
authentication::Mechanism,
error::{self, Error},
response::Response,
util::XText,
}; };
/// Client identifier, the parameter to `EHLO` /// Client identifier, the parameter to `EHLO`
@@ -49,15 +52,16 @@ impl Default for ClientId {
impl Display for ClientId { impl Display for ClientId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match self {
Self::Domain(ref value) => f.write_str(value), Self::Domain(value) => f.write_str(value),
Self::Ipv4(ref value) => write!(f, "[{}]", value), Self::Ipv4(value) => write!(f, "[{value}]"),
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value), Self::Ipv6(value) => write!(f, "[IPv6:{value}]"),
} }
} }
} }
impl ClientId { impl ClientId {
#[doc(hidden)]
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")] #[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
/// Creates a new `ClientId` from a fully qualified domain name /// Creates a new `ClientId` from a fully qualified domain name
pub fn new(domain: String) -> Self { pub fn new(domain: String) -> Self {
@@ -68,6 +72,7 @@ impl ClientId {
/// Supported ESMTP keywords /// Supported ESMTP keywords
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Extension { pub enum Extension {
/// 8BITMIME keyword /// 8BITMIME keyword
/// ///
@@ -87,11 +92,11 @@ pub enum Extension {
impl Display for Extension { impl Display for Extension {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match self {
Extension::EightBitMime => f.write_str("8BITMIME"), Extension::EightBitMime => f.write_str("8BITMIME"),
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"), Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
Extension::StartTls => f.write_str("STARTTLS"), Extension::StartTls => f.write_str("STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism), Extension::Authentication(mechanism) => write!(f, "AUTH {mechanism}"),
} }
} }
} }
@@ -103,17 +108,17 @@ pub struct ServerInfo {
/// Server name /// Server name
/// ///
/// The name given in the server banner /// The name given in the server banner
pub name: String, name: String,
/// ESMTP features supported by the server /// ESMTP features supported by the server
/// ///
/// It contains the features supported by the server and known by the `Extension` module. /// It contains the features supported by the server and known by the `Extension` module.
pub features: HashSet<Extension>, features: HashSet<Extension>,
} }
impl Display for ServerInfo { impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let features = if self.features.is_empty() { let features = if self.features.is_empty() {
"no supported features".to_string() "no supported features".to_owned()
} else { } else {
format!("{:?}", self.features) format!("{:?}", self.features)
}; };
@@ -126,12 +131,12 @@ impl ServerInfo {
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> { pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() { let name = match response.first_word() {
Some(name) => name, Some(name) => name,
None => return Err(Error::ResponseParsing("Could not read server name")), None => return Err(error::response("Could not read server name")),
}; };
let mut features: HashSet<Extension> = HashSet::new(); let mut features: HashSet<Extension> = HashSet::new();
for line in response.message.as_slice() { for line in response.message() {
if line.is_empty() { if line.is_empty() {
continue; continue;
} }
@@ -168,7 +173,7 @@ impl ServerInfo {
} }
Ok(ServerInfo { Ok(ServerInfo {
name: name.to_string(), name: name.to_owned(),
features, features,
}) })
} }
@@ -184,7 +189,7 @@ impl ServerInfo {
.contains(&Extension::Authentication(mechanism)) .contains(&Extension::Authentication(mechanism))
} }
/// Gets a compatible mechanism from list /// Gets a compatible mechanism from a list
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> { pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
for mechanism in mechanisms { for mechanism in mechanisms {
if self.supports_auth_mechanism(*mechanism) { if self.supports_auth_mechanism(*mechanism) {
@@ -193,6 +198,11 @@ impl ServerInfo {
} }
None None
} }
/// The name given in the server banner
pub fn name(&self) -> &str {
self.name.as_ref()
}
} }
/// A `MAIL FROM` extension parameter /// A `MAIL FROM` extension parameter
@@ -216,16 +226,16 @@ pub enum MailParameter {
impl Display for MailParameter { impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match self {
MailParameter::Body(ref value) => write!(f, "BODY={}", value), MailParameter::Body(value) => write!(f, "BODY={value}"),
MailParameter::Size(size) => write!(f, "SIZE={}", size), MailParameter::Size(size) => write!(f, "SIZE={size}"),
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"), MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
MailParameter::Other { MailParameter::Other {
ref keyword, keyword,
value: Some(ref value), value: Some(value),
} => write!(f, "{}={}", keyword, XText(value)), } => write!(f, "{}={}", keyword, XText(value)),
MailParameter::Other { MailParameter::Other {
ref keyword, keyword,
value: None, value: None,
} => f.write_str(keyword), } => f.write_str(keyword),
} }
@@ -266,13 +276,13 @@ pub enum RcptParameter {
impl Display for RcptParameter { impl Display for RcptParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match &self {
RcptParameter::Other { RcptParameter::Other {
ref keyword, keyword,
value: Some(ref value), value: Some(value),
} => write!(f, "{}={}", keyword, XText(value)), } => write!(f, "{keyword}={}", XText(value)),
RcptParameter::Other { RcptParameter::Other {
ref keyword, keyword,
value: None, value: None,
} => f.write_str(keyword), } => f.write_str(keyword),
} }
@@ -282,31 +292,32 @@ impl Display for RcptParameter {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::collections::HashSet;
use super::*; use super::*;
use crate::transport::smtp::{ use crate::transport::smtp::{
authentication::Mechanism, authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity}, response::{Category, Code, Detail, Response, Severity},
}; };
use std::collections::HashSet;
#[test] #[test]
fn test_clientid_fmt() { fn test_clientid_fmt() {
assert_eq!( assert_eq!(
format!("{}", ClientId::Domain("test".to_string())), format!("{}", ClientId::Domain("test".to_owned())),
"test".to_string() "test".to_owned()
); );
assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string()); assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_owned());
} }
#[test] #[test]
fn test_extension_fmt() { fn test_extension_fmt() {
assert_eq!( assert_eq!(
format!("{}", Extension::EightBitMime), format!("{}", Extension::EightBitMime),
"8BITMIME".to_string() "8BITMIME".to_owned()
); );
assert_eq!( assert_eq!(
format!("{}", Extension::Authentication(Mechanism::Plain)), format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string() "AUTH PLAIN".to_owned()
); );
} }
@@ -319,11 +330,11 @@ mod test {
format!( format!(
"{}", "{}",
ServerInfo { ServerInfo {
name: "name".to_string(), name: "name".to_owned(),
features: eightbitmime, features: eightbitmime,
} }
), ),
"name with {EightBitMime}".to_string() "name with {EightBitMime}".to_owned()
); );
let empty = HashSet::new(); let empty = HashSet::new();
@@ -332,11 +343,11 @@ mod test {
format!( format!(
"{}", "{}",
ServerInfo { ServerInfo {
name: "name".to_string(), name: "name".to_owned(),
features: empty, features: empty,
} }
), ),
"name with no supported features".to_string() "name with no supported features".to_owned()
); );
let mut plain = HashSet::new(); let mut plain = HashSet::new();
@@ -346,11 +357,11 @@ mod test {
format!( format!(
"{}", "{}",
ServerInfo { ServerInfo {
name: "name".to_string(), name: "name".to_owned(),
features: plain, features: plain,
} }
), ),
"name with {Authentication(Plain)}".to_string() "name with {Authentication(Plain)}".to_owned()
); );
} }
@@ -362,18 +373,14 @@ mod test {
Category::Unspecified4, Category::Unspecified4,
Detail::One, Detail::One,
), ),
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
); );
let mut features = HashSet::new(); let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime)); assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo { let server_info = ServerInfo {
name: "me".to_string(), name: "me".to_owned(),
features, features,
}; };
@@ -389,10 +396,10 @@ mod test {
Detail::One, Detail::One,
), ),
vec![ vec![
"me".to_string(), "me".to_owned(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(), "AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
], ],
); );
@@ -402,7 +409,7 @@ mod test {
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),)); assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
let server_info2 = ServerInfo { let server_info2 = ServerInfo {
name: "me".to_string(), name: "me".to_owned(),
features: features2, features: features2,
}; };

View File

@@ -33,7 +33,7 @@
//! ```rust,no_run //! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))] //! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> { //! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport}; //! use lettre::{Message, SmtpTransport, Transport};
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -43,8 +43,7 @@
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! // Create TLS transport on port 465 //! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")? //! let sender = SmtpTransport::relay("smtp.example.com")?.build();
//! .build();
//! // Send the email via remote relay //! // Send the email via remote relay
//! let result = sender.send(&email); //! let result = sender.send(&email);
//! assert!(result.is_ok()); //! assert!(result.is_ok());
@@ -59,7 +58,13 @@
//! ```rust,no_run //! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))] //! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> { //! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::{PoolConfig, authentication::{Credentials, Mechanism}}}; //! use lettre::{
//! transport::smtp::{
//! authentication::{Credentials, Mechanism},
//! PoolConfig,
//! },
//! Message, SmtpTransport, Transport,
//! };
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -71,7 +76,10 @@
//! // Create TLS transport on port 587 with STARTTLS //! // Create TLS transport on port 587 with STARTTLS
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")? //! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Add credentials for authentication //! // Add credentials for authentication
//! .credentials(Credentials::new("username".to_string(), "password".to_string())) //! .credentials(Credentials::new(
//! "username".to_owned(),
//! "password".to_owned(),
//! ))
//! // Configure expected authentication mechanism //! // Configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain]) //! .authentication(vec![Mechanism::Plain])
//! // Connection pool settings //! // Connection pool settings
@@ -90,7 +98,10 @@
//! ```rust,no_run //! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))] //! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> { //! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::client::{TlsParameters, Tls}}; //! use lettre::{
//! transport::smtp::client::{Tls, TlsParameters},
//! Message, SmtpTransport, Transport,
//! };
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -100,8 +111,9 @@
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! // Custom TLS configuration //! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_string()) //! let tls = TlsParameters::builder("smtp.example.com".to_owned())
//! .dangerous_accept_invalid_certs(true).build()?; //! .dangerous_accept_invalid_certs(true)
//! .build()?;
//! //!
//! // Create TLS transport on port 465 //! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")? //! let sender = SmtpTransport::relay("smtp.example.com")?
@@ -116,19 +128,19 @@
//! # } //! # }
//! ``` //! ```
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))] use std::time::Duration;
pub use self::async_transport::{
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder, use client::Tls;
};
#[cfg(feature = "r2d2")] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
#[cfg(feature = "pool")]
pub use self::pool::PoolConfig; pub use self::pool::PoolConfig;
#[cfg(feature = "r2d2")]
pub(crate) use self::transport::SmtpClient;
pub use self::{ pub use self::{
error::Error, error::Error,
transport::{SmtpTransport, SmtpTransportBuilder}, transport::{SmtpTransport, SmtpTransportBuilder},
}; };
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use crate::transport::smtp::client::TlsParameters; use crate::transport::smtp::client::TlsParameters;
use crate::transport::smtp::{ use crate::transport::smtp::{
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS}, authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
@@ -136,34 +148,20 @@ use crate::transport::smtp::{
extension::ClientId, extension::ClientId,
response::Response, response::Response,
}; };
use client::Tls;
use std::time::Duration;
#[doc(hidden)] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[allow(deprecated)]
#[cfg(feature = "async-std1")]
pub use self::async_transport::AsyncStd1Connector;
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(feature = "tokio02")]
pub use self::async_transport::Tokio02Connector;
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(feature = "tokio1")]
pub use self::async_transport::Tokio1Connector;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
mod async_transport; mod async_transport;
pub mod authentication; pub mod authentication;
pub mod client; pub mod client;
pub mod commands; pub mod commands;
mod connection_url;
mod error; mod error;
pub mod extension; pub mod extension;
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
mod pool; mod pool;
pub mod response; pub mod response;
mod transport; mod transport;
pub mod util; pub(super) mod util;
// Registered port numbers: // Registered port numbers:
// https://www.iana. // https://www.iana.
@@ -179,10 +177,9 @@ pub const SUBMISSION_PORT: u16 = 587;
pub const SUBMISSIONS_PORT: u16 = 465; pub const SUBMISSIONS_PORT: u16 = 465;
/// Default timeout /// Default timeout
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
#[allow(missing_debug_implementations)] #[derive(Debug, Clone)]
#[derive(Clone)]
struct SmtpInfo { struct SmtpInfo {
/// Name sent during EHLO /// Name sent during EHLO
hello_name: ClientId, hello_name: ClientId,
@@ -204,7 +201,7 @@ struct SmtpInfo {
impl Default for SmtpInfo { impl Default for SmtpInfo {
fn default() -> Self { fn default() -> Self {
Self { Self {
server: "localhost".to_string(), server: "localhost".to_owned(),
port: SMTP_PORT, port: SMTP_PORT,
hello_name: ClientId::default(), hello_name: ClientId::default(),
credentials: None, credentials: None,

View File

@@ -1,95 +0,0 @@
use std::time::Duration;
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
use r2d2::{ManageConnection, Pool};
/// Configuration for a connection pool
#[derive(Debug, Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
pub struct PoolConfig {
min_idle: u32,
max_size: u32,
connection_timeout: Duration,
idle_timeout: Duration,
}
impl PoolConfig {
/// Create a new pool configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Minimum number of idle connections
///
/// Defaults to `0`
pub fn min_idle(mut self, min_idle: u32) -> Self {
self.min_idle = min_idle;
self
}
/// Maximum number of pooled connections
///
/// Defaults to `10`
pub fn max_size(mut self, max_size: u32) -> Self {
self.max_size = max_size;
self
}
/// Connection timeout
///
/// Defaults to `30 seconds`
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
self.connection_timeout = connection_timeout;
self
}
/// Connection idle timeout
///
/// Defaults to `60 seconds`
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
self.idle_timeout = idle_timeout;
self
}
pub(crate) fn build<C: ManageConnection>(&self, client: C) -> Pool<C> {
Pool::builder()
.min_idle(Some(self.min_idle))
.max_size(self.max_size)
.connection_timeout(self.connection_timeout)
.idle_timeout(Some(self.idle_timeout))
.build_unchecked(client)
}
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
min_idle: 0,
max_size: 10,
connection_timeout: Duration::from_secs(30),
idle_timeout: Duration::from_secs(60),
}
}
}
impl ManageConnection for SmtpClient {
type Connection = SmtpConnection;
type Error = Error;
fn connect(&self) -> Result<Self::Connection, Error> {
self.connection()
}
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
if conn.test_connected() {
return Ok(());
}
Err(Error::Client("is not connected anymore"))
}
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
conn.has_broken()
}
}

View File

@@ -0,0 +1,300 @@
use std::{
fmt::{self, Debug},
mem,
ops::{Deref, DerefMut},
sync::{Arc, OnceLock},
time::{Duration, Instant},
};
use futures_util::{
lock::Mutex,
stream::{self, StreamExt},
};
use super::{
super::{client::AsyncSmtpConnection, Error},
PoolConfig,
};
use crate::{executor::SpawnHandle, transport::smtp::async_transport::AsyncSmtpClient, Executor};
pub struct Pool<E: Executor> {
config: PoolConfig,
connections: Mutex<Vec<ParkedConnection>>,
client: AsyncSmtpClient<E>,
handle: OnceLock<E::Handle>,
}
struct ParkedConnection {
conn: AsyncSmtpConnection,
since: Instant,
}
pub struct PooledConnection<E: Executor> {
conn: Option<AsyncSmtpConnection>,
pool: Arc<Pool<E>>,
}
impl<E: Executor> Pool<E> {
pub fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
let pool = Arc::new(Self {
config,
connections: Mutex::new(Vec::new()),
client,
handle: OnceLock::new(),
});
{
let pool_ = Arc::clone(&pool);
let min_idle = pool_.config.min_idle;
let idle_timeout = pool_.config.idle_timeout;
let pool = Arc::downgrade(&pool_);
let handle = E::spawn(async move {
loop {
#[cfg(feature = "tracing")]
tracing::trace!("running cleanup tasks");
match pool.upgrade() {
Some(pool) => {
#[allow(clippy::needless_collect)]
let (count, dropped) = {
let mut connections = pool.connections.lock().await;
let to_drop = connections
.iter()
.enumerate()
.rev()
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
.map(|(i, _)| i)
.collect::<Vec<_>>();
let dropped = to_drop
.into_iter()
.map(|i| connections.remove(i))
.collect::<Vec<_>>();
(connections.len(), dropped)
};
#[cfg(feature = "tracing")]
let mut created = 0;
for _ in count..=(min_idle as usize) {
let conn = match pool.client.connection().await {
Ok(conn) => conn,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::warn!("couldn't create idle connection {}", err);
#[cfg(not(feature = "tracing"))]
let _ = err;
break;
}
};
let mut connections = pool.connections.lock().await;
connections.push(ParkedConnection::park(conn));
#[cfg(feature = "tracing")]
{
created += 1;
}
}
#[cfg(feature = "tracing")]
if created > 0 {
tracing::debug!("created {} idle connections", created);
}
if !dropped.is_empty() {
#[cfg(feature = "tracing")]
tracing::debug!("dropped {} idle connections", dropped.len());
abort_concurrent(dropped.into_iter().map(|conn| conn.unpark()))
.await;
}
}
None => {
#[cfg(feature = "tracing")]
tracing::warn!(
"breaking out of task - no more references to Pool are available"
);
break;
}
}
E::sleep(idle_timeout).await;
}
});
pool_
.handle
.set(handle)
.expect("handle hasn't been set yet");
}
pool
}
pub async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
loop {
let conn = {
let mut connections = self.connections.lock().await;
connections.pop()
};
match conn {
Some(conn) => {
let mut conn = conn.unpark();
// TODO: handle the client try another connection if this one isn't good
if !conn.test_connected().await {
#[cfg(feature = "tracing")]
tracing::debug!("dropping a broken connection");
conn.abort().await;
continue;
}
#[cfg(feature = "tracing")]
tracing::debug!("reusing a pooled connection");
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
}
None => {
#[cfg(feature = "tracing")]
tracing::debug!("creating a new connection");
let conn = self.client.connection().await?;
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
}
}
}
}
async fn recycle(&self, mut conn: AsyncSmtpConnection) {
if conn.has_broken() {
#[cfg(feature = "tracing")]
tracing::debug!("dropping a broken connection instead of recycling it");
conn.abort().await;
drop(conn);
} else {
#[cfg(feature = "tracing")]
tracing::debug!("recycling connection");
let mut connections = self.connections.lock().await;
if connections.len() >= self.config.max_size as usize {
drop(connections);
conn.abort().await;
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
}
}
}
}
impl<E: Executor> Debug for Pool<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pool")
.field("config", &self.config)
.field(
"connections",
&match self.connections.try_lock() {
Some(connections) => format!("{} connections", connections.len()),
None => "LOCKED".to_owned(),
},
)
.field("client", &self.client)
.field(
"handle",
&match self.handle.get() {
Some(_) => "Some(JoinHandle)",
None => "None",
},
)
.finish()
}
}
impl<E: Executor> Drop for Pool<E> {
fn drop(&mut self) {
#[cfg(feature = "tracing")]
tracing::debug!("dropping Pool");
let connections = mem::take(self.connections.get_mut());
let handle = self.handle.take();
E::spawn(async move {
if let Some(handle) = handle {
handle.shutdown().await;
}
abort_concurrent(connections.into_iter().map(|conn| conn.unpark())).await;
});
}
}
impl ParkedConnection {
fn park(conn: AsyncSmtpConnection) -> Self {
Self {
conn,
since: Instant::now(),
}
}
fn idle_duration(&self) -> Duration {
self.since.elapsed()
}
fn unpark(self) -> AsyncSmtpConnection {
self.conn
}
}
impl<E: Executor> PooledConnection<E> {
fn wrap(conn: AsyncSmtpConnection, pool: Arc<Pool<E>>) -> Self {
Self {
conn: Some(conn),
pool,
}
}
}
impl<E: Executor> Deref for PooledConnection<E> {
type Target = AsyncSmtpConnection;
fn deref(&self) -> &Self::Target {
self.conn.as_ref().expect("conn hasn't been dropped yet")
}
}
impl<E: Executor> DerefMut for PooledConnection<E> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.conn.as_mut().expect("conn hasn't been dropped yet")
}
}
impl<E: Executor> Drop for PooledConnection<E> {
fn drop(&mut self) {
let conn = self
.conn
.take()
.expect("AsyncSmtpConnection hasn't been taken yet");
let pool = Arc::clone(&self.pool);
E::spawn(async move {
pool.recycle(conn).await;
});
}
}
async fn abort_concurrent<I>(iter: I)
where
I: Iterator<Item = AsyncSmtpConnection>,
{
stream::iter(iter)
.for_each_concurrent(8, |mut conn| async move {
conn.abort().await;
})
.await;
}

View File

@@ -0,0 +1,66 @@
use std::time::Duration;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub mod async_impl;
pub mod sync_impl;
/// Configuration for a connection pool
#[derive(Debug, Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
pub struct PoolConfig {
min_idle: u32,
max_size: u32,
idle_timeout: Duration,
}
impl PoolConfig {
/// Create a new pool configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Minimum number of idle connections
///
/// Defaults to `0`
pub fn min_idle(mut self, min_idle: u32) -> Self {
self.min_idle = min_idle;
self
}
/// Maximum number of pooled connections
///
/// Defaults to `10`
pub fn max_size(mut self, max_size: u32) -> Self {
self.max_size = max_size;
self
}
/// Connection timeout
///
/// Defaults to `30 seconds`
#[doc(hidden)]
#[deprecated(note = "The Connection timeout is already configured on the SMTP transport")]
pub fn connection_timeout(self, connection_timeout: Duration) -> Self {
let _ = connection_timeout;
self
}
/// Connection idle timeout
///
/// Defaults to `60 seconds`
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
self.idle_timeout = idle_timeout;
self
}
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
min_idle: 0,
max_size: 10,
idle_timeout: Duration::from_secs(60),
}
}
}

View File

@@ -0,0 +1,259 @@
use std::{
fmt::{self, Debug},
mem,
ops::{Deref, DerefMut},
sync::{Arc, Mutex, TryLockError},
thread,
time::{Duration, Instant},
};
use super::{
super::{client::SmtpConnection, Error},
PoolConfig,
};
use crate::transport::smtp::transport::SmtpClient;
pub struct Pool {
config: PoolConfig,
connections: Mutex<Vec<ParkedConnection>>,
client: SmtpClient,
}
struct ParkedConnection {
conn: SmtpConnection,
since: Instant,
}
pub struct PooledConnection {
conn: Option<SmtpConnection>,
pool: Arc<Pool>,
}
impl Pool {
pub fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
let pool = Arc::new(Self {
config,
connections: Mutex::new(Vec::new()),
client,
});
{
let pool_ = Arc::clone(&pool);
let min_idle = pool_.config.min_idle;
let idle_timeout = pool_.config.idle_timeout;
let pool = Arc::downgrade(&pool_);
thread::Builder::new()
.name("lettre-connection-pool".into())
.spawn(move || {
while let Some(pool) = pool.upgrade() {
#[cfg(feature = "tracing")]
tracing::trace!("running cleanup tasks");
#[allow(clippy::needless_collect)]
let (count, dropped) = {
let mut connections = pool.connections.lock().unwrap();
let to_drop = connections
.iter()
.enumerate()
.rev()
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
.map(|(i, _)| i)
.collect::<Vec<_>>();
let dropped = to_drop
.into_iter()
.map(|i| connections.remove(i))
.collect::<Vec<_>>();
(connections.len(), dropped)
};
#[cfg(feature = "tracing")]
let mut created = 0;
for _ in count..=(min_idle as usize) {
let conn = match pool.client.connection() {
Ok(conn) => conn,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::warn!("couldn't create idle connection {}", err);
#[cfg(not(feature = "tracing"))]
let _ = err;
break;
}
};
let mut connections = pool.connections.lock().unwrap();
connections.push(ParkedConnection::park(conn));
#[cfg(feature = "tracing")]
{
created += 1;
}
}
#[cfg(feature = "tracing")]
if created > 0 {
tracing::debug!("created {} idle connections", created);
}
if !dropped.is_empty() {
#[cfg(feature = "tracing")]
tracing::debug!("dropped {} idle connections", dropped.len());
for conn in dropped {
let mut conn = conn.unpark();
conn.abort();
}
}
thread::sleep(idle_timeout);
}
})
.expect("couldn't spawn the Pool thread");
}
pool
}
pub fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
loop {
let conn = {
let mut connections = self.connections.lock().unwrap();
connections.pop()
};
match conn {
Some(conn) => {
let mut conn = conn.unpark();
// TODO: handle the client try another connection if this one isn't good
if !conn.test_connected() {
#[cfg(feature = "tracing")]
tracing::debug!("dropping a broken connection");
conn.abort();
continue;
}
#[cfg(feature = "tracing")]
tracing::debug!("reusing a pooled connection");
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
}
None => {
#[cfg(feature = "tracing")]
tracing::debug!("creating a new connection");
let conn = self.client.connection()?;
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
}
}
}
}
fn recycle(&self, mut conn: SmtpConnection) {
if conn.has_broken() {
#[cfg(feature = "tracing")]
tracing::debug!("dropping a broken connection instead of recycling it");
conn.abort();
drop(conn);
} else {
#[cfg(feature = "tracing")]
tracing::debug!("recycling connection");
let mut connections = self.connections.lock().unwrap();
if connections.len() >= self.config.max_size as usize {
drop(connections);
conn.abort();
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
}
}
}
}
impl Debug for Pool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pool")
.field("config", &self.config)
.field(
"connections",
&match self.connections.try_lock() {
Ok(connections) => format!("{} connections", connections.len()),
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
},
)
.field("client", &self.client)
.finish()
}
}
impl Drop for Pool {
fn drop(&mut self) {
#[cfg(feature = "tracing")]
tracing::debug!("dropping Pool");
let connections = mem::take(&mut *self.connections.get_mut().unwrap());
for conn in connections {
let mut conn = conn.unpark();
conn.abort();
}
}
}
impl ParkedConnection {
fn park(conn: SmtpConnection) -> Self {
Self {
conn,
since: Instant::now(),
}
}
fn idle_duration(&self) -> Duration {
self.since.elapsed()
}
fn unpark(self) -> SmtpConnection {
self.conn
}
}
impl PooledConnection {
fn wrap(conn: SmtpConnection, pool: Arc<Pool>) -> Self {
Self {
conn: Some(conn),
pool,
}
}
}
impl Deref for PooledConnection {
type Target = SmtpConnection;
fn deref(&self) -> &Self::Target {
self.conn.as_ref().expect("conn hasn't been dropped yet")
}
}
impl DerefMut for PooledConnection {
fn deref_mut(&mut self) -> &mut Self::Target {
self.conn.as_mut().expect("conn hasn't been dropped yet")
}
}
impl Drop for PooledConnection {
fn drop(&mut self) {
let conn = self
.conn
.take()
.expect("SmtpConnection hasn't been taken yet");
self.pool.recycle(conn);
}
}

View File

@@ -1,7 +1,12 @@
//! SMTP response, containing a mandatory return code and an optional text //! SMTP response, containing a mandatory return code and an optional text
//! message //! message
use crate::transport::smtp::Error; use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
};
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag, take_until}, bytes::streaming::{tag, take_until},
@@ -10,14 +15,10 @@ use nom::{
sequence::{preceded, tuple}, sequence::{preceded, tuple},
IResult, IResult,
}; };
use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
string::ToString,
};
/// First digit indicates severity use crate::transport::smtp::{error, Error};
/// The first digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)] #[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity { pub enum Severity {
@@ -120,6 +121,20 @@ impl Code {
detail, detail,
} }
} }
/// Tells if the response is positive
pub fn is_positive(self) -> bool {
matches!(
self.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
}
}
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 /// Contains an SMTP reply, with separated code and message
@@ -129,17 +144,19 @@ impl Code {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Response { pub struct Response {
/// Response code /// Response code
pub code: Code, code: Code,
/// Server response string (optional) /// Server response string (optional)
/// Handle multiline responses /// Handle multiline responses
pub message: Vec<String>, message: Vec<String>,
} }
impl FromStr for Response { impl FromStr for Response {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> result::Result<Response, Error> { fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s).map(|(_, r)| r).map_err(|e| e.into()) parse_response(s)
.map(|(_, r)| r)
.map_err(|e| error::response(e.to_owned()))
} }
} }
@@ -151,10 +168,7 @@ impl Response {
/// Tells if the response is positive /// Tells if the response is positive
pub fn is_positive(&self) -> bool { pub fn is_positive(&self) -> bool {
matches!( self.code.is_positive()
self.code.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
} }
/// Tests code equality /// Tests code equality
@@ -173,6 +187,16 @@ impl Response {
pub fn first_line(&self) -> Option<&str> { pub fn first_line(&self) -> Option<&str> {
self.message.first().map(String::as_str) self.message.first().map(String::as_str)
} }
/// Response code
pub fn code(&self) -> Code {
self.code
}
/// Server response string (array of lines)
pub fn message(&self) -> impl Iterator<Item = &str> {
self.message.iter().map(String::as_str)
}
} }
// Parsers (originally from tokio-smtp) // Parsers (originally from tokio-smtp)
@@ -298,6 +322,17 @@ mod test {
assert_eq!(code.to_string(), "421"); 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] #[test]
fn test_response_from_str() { 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"; let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
@@ -310,10 +345,10 @@ mod test {
detail: Detail::Zero, detail: Detail::Zero,
}, },
message: vec![ message: vec![
"me".to_string(), "me".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
"AUTH PLAIN CRAM-MD5".to_string(), "AUTH PLAIN CRAM-MD5".to_owned(),
], ],
} }
); );
@@ -333,11 +368,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::Zero, detail: Detail::Zero,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.is_positive()); .is_positive());
assert!(!Response::new( assert!(!Response::new(
@@ -346,11 +377,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::Zero, detail: Detail::Zero,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.is_positive()); .is_positive());
} }
@@ -363,11 +390,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.has_code(451)); .has_code(451));
assert!(!Response::new( assert!(!Response::new(
@@ -376,11 +399,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.has_code(251)); .has_code(251));
} }
@@ -394,11 +413,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.first_word(), .first_word(),
Some("me") Some("me")
@@ -411,9 +426,9 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec![
"me mo".to_string(), "me mo".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
], ],
) )
.first_word(), .first_word(),
@@ -438,7 +453,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_word(), .first_word(),
None None
@@ -450,7 +465,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_word(), .first_word(),
None None
@@ -462,7 +477,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec!["".to_string()], vec!["".to_owned()],
) )
.first_word(), .first_word(),
None None
@@ -475,7 +490,7 @@ mod test {
let res = parse_response(raw_response); let res = parse_response(raw_response);
match res { match res {
Err(nom::Err::Incomplete(_)) => {} Err(nom::Err::Incomplete(_)) => {}
_ => panic!("Expected incomplete response, got {:?}", res), _ => panic!("Expected incomplete response, got {res:?}"),
} }
} }
@@ -488,11 +503,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.first_line(), .first_line(),
Some("me") Some("me")
@@ -505,9 +516,9 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec![
"me mo".to_string(), "me mo".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
], ],
) )
.first_line(), .first_line(),
@@ -532,7 +543,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_line(), .first_line(),
Some(" ") Some(" ")
@@ -544,7 +555,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_line(), .first_line(),
Some(" ") Some(" ")
@@ -556,7 +567,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec!["".to_string()], vec!["".to_owned()],
) )
.first_line(), .first_line(),
Some("") Some("")

View File

@@ -1,22 +1,23 @@
use std::time::Duration; #[cfg(feature = "pool")]
use std::sync::Arc;
use std::{fmt::Debug, time::Duration};
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
use r2d2::Pool; use super::pool::sync_impl::Pool;
#[cfg(feature = "pool")]
#[cfg(feature = "r2d2")]
use super::PoolConfig; use super::PoolConfig;
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo}; use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT}; use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::{address::Envelope, Transport}; use crate::{address::Envelope, Transport};
#[allow(missing_debug_implementations)] /// Sends emails using the SMTP protocol
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
#[derive(Clone)] #[derive(Clone)]
/// Transport using the SMTP protocol
pub struct SmtpTransport { pub struct SmtpTransport {
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
inner: Pool<SmtpClient>, inner: Arc<Pool>,
#[cfg(not(feature = "r2d2"))] #[cfg(not(feature = "pool"))]
inner: SmtpClient, inner: SmtpClient,
} }
@@ -26,20 +27,25 @@ impl Transport for SmtpTransport {
/// Sends an email /// Sends an email
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> { fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "r2d2")]
let mut conn = self.inner.get()?;
#[cfg(not(feature = "r2d2"))]
let mut conn = self.inner.connection()?; let mut conn = self.inner.connection()?;
let result = conn.send(envelope, email)?; let result = conn.send(envelope, email)?;
#[cfg(not(feature = "r2d2"))] #[cfg(not(feature = "pool"))]
conn.quit()?; conn.quit()?;
Ok(result) Ok(result)
} }
} }
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 { impl SmtpTransport {
/// Simple and secure transport, using TLS connections to communicate with the SMTP server /// Simple and secure transport, using TLS connections to communicate with the SMTP server
/// ///
@@ -47,7 +53,11 @@ impl SmtpTransport {
/// ///
/// Creates an encrypted transport over submissions port, using the provided domain /// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates. /// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[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 relay(relay: &str) -> Result<SmtpTransportBuilder, Error> { pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?; let tls_parameters = TlsParameters::new(relay.into())?;
@@ -56,7 +66,7 @@ impl SmtpTransport {
.tls(Tls::Wrapper(tls_parameters))) .tls(Tls::Wrapper(tls_parameters)))
} }
/// Simple an secure transport, using STARTTLS to obtain encrypted connections /// Simple and secure transport, using STARTTLS to obtain encrypted connections
/// ///
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers /// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections. /// that don't take SMTPS connections.
@@ -67,7 +77,11 @@ impl SmtpTransport {
/// ///
/// An error is returned if the connection can't be upgraded. No credentials /// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks. /// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[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 starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> { pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?; let tls_parameters = TlsParameters::new(relay.into())?;
@@ -89,38 +103,143 @@ impl SmtpTransport {
/// ///
/// * No authentication /// * No authentication
/// * No TLS /// * No TLS
/// * A 60 seconds timeout for smtp commands /// * A 60-seconds timeout for smtp commands
/// * Port 25 /// * Port 25
/// ///
/// Consider using [`SmtpTransport::relay`](#method.relay) or /// Consider using [`SmtpTransport::relay`](#method.relay) or
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead, /// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible. /// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder { pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let new = SmtpInfo { SmtpTransportBuilder::new(server)
server: server.into(),
..Default::default()
};
SmtpTransportBuilder {
info: new,
#[cfg(feature = "r2d2")]
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 afterward if a connection pool is not used.
pub fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection()?;
let is_connected = conn.test_connected();
#[cfg(not(feature = "pool"))]
conn.quit()?;
Ok(is_connected)
} }
} }
/// Contains client configuration. /// Contains client configuration.
/// Instances of this struct can be created using functions of [`SmtpTransport`]. /// Instances of this struct can be created using functions of [`SmtpTransport`].
#[allow(missing_debug_implementations)] #[derive(Debug, Clone)]
#[derive(Clone)]
pub struct SmtpTransportBuilder { pub struct SmtpTransportBuilder {
info: SmtpInfo, info: SmtpInfo,
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
pool_config: PoolConfig, pool_config: PoolConfig,
} }
/// Builder for the SMTP `SmtpTransport` /// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder { impl SmtpTransportBuilder {
// Create new builder with default parameters
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
let new = SmtpInfo {
server: server.into(),
..Default::default()
};
Self {
info: new,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Set the name used during EHLO /// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self { pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name; self.info.hello_name = name;
@@ -152,7 +271,11 @@ impl SmtpTransportBuilder {
} }
/// Set the TLS settings to use /// Set the TLS settings to use
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[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 tls(mut self, tls: Tls) -> Self { pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls; self.info.tls = tls;
self self
@@ -161,8 +284,8 @@ impl SmtpTransportBuilder {
/// Use a custom configuration for the connection pool /// Use a custom configuration for the connection pool
/// ///
/// Defaults can be found at [`PoolConfig`] /// Defaults can be found at [`PoolConfig`]
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))] #[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self { pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
self.pool_config = pool_config; self.pool_config = pool_config;
self self
@@ -170,21 +293,20 @@ impl SmtpTransportBuilder {
/// Build the transport /// Build the transport
/// ///
/// If the `r2d2` feature is enabled an `Arc` wrapped pool is be created. /// If the `pool` feature is enabled, an `Arc` wrapped pool is created.
/// Defaults can be found at [`PoolConfig`] /// Defaults can be found at [`PoolConfig`]
pub fn build(self) -> SmtpTransport { pub fn build(self) -> SmtpTransport {
let client = SmtpClient { info: self.info }; let client = SmtpClient { info: self.info };
SmtpTransport {
#[cfg(feature = "r2d2")] #[cfg(feature = "pool")]
inner: self.pool_config.build(client), let client = Pool::new(self.pool_config, client);
#[cfg(not(feature = "r2d2"))]
inner: client, SmtpTransport { inner: client }
}
} }
} }
/// Build client /// Build client
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct SmtpClient { pub struct SmtpClient {
info: SmtpInfo, info: SmtpInfo,
} }
@@ -195,9 +317,9 @@ impl SmtpClient {
/// Handles encryption and authentication /// Handles encryption and authentication
pub fn connection(&self) -> Result<SmtpConnection, Error> { pub fn connection(&self) -> Result<SmtpConnection, Error> {
#[allow(clippy::match_single_binding)] #[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"))] #[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, _ => None,
}; };
@@ -207,24 +329,100 @@ impl SmtpClient {
self.info.timeout, self.info.timeout,
&self.info.hello_name, &self.info.hello_name,
tls_parameters, tls_parameters,
None,
)?; )?;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
match self.info.tls { match &self.info.tls {
Tls::Opportunistic(ref tls_parameters) => { Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() { 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) => { Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters, &self.info.hello_name)?; conn = conn.starttls(tls_parameters, &self.info.hello_name)?;
} }
_ => (), _ => (),
} }
if let Some(credentials) = &self.info.credentials { if let Some(credentials) = &self.info.credentials {
conn.auth(&self.info.authentication, &credentials)?; conn.auth(&self.info.authentication, credentials)?;
} }
Ok(conn) Ok(conn)
} }
} }
#[cfg(test)]
mod tests {
use crate::{
transport::smtp::{authentication::Credentials, client::Tls},
SmtpTransport,
};
#[test]
fn transport_from_url() {
let builder = SmtpTransport::from_url("smtp://127.0.0.1:2525").unwrap();
assert_eq!(builder.info.port, 2525);
assert!(matches!(builder.info.tls, Tls::None));
assert_eq!(builder.info.server, "127.0.0.1");
let builder =
SmtpTransport::from_url("smtps://username:password@smtp.example.com:465").unwrap();
assert_eq!(builder.info.port, 465);
assert_eq!(
builder.info.credentials,
Some(Credentials::new(
"username".to_owned(),
"password".to_owned()
))
);
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
assert_eq!(builder.info.server, "smtp.example.com");
let builder = SmtpTransport::from_url(
"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(_)));
}
}

View File

@@ -4,7 +4,6 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
/// Encode a string as xtext /// Encode a string as xtext
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct XText<'a>(pub &'a str); pub struct XText<'a>(pub &'a str);
impl<'a> Display for XText<'a> { impl<'a> Display for XText<'a> {
@@ -42,7 +41,7 @@ mod tests {
] ]
.iter() .iter()
{ {
assert_eq!(format!("{}", XText(input)), expect.to_string()); assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
} }
} }
} }

View File

@@ -1,10 +1,12 @@
//! The stub transport only logs message envelope and drops the content. It can be useful for //! The stub transport logs message envelopes as well as contents. It can be useful for testing
//! testing purposes. //! purposes.
//! //!
//! #### Stub Transport //! # Stub Transport
//! //!
//! The stub transport returns provided result and drops the content. It can be useful for //! The stub transport logs message envelopes as well as contents. It can be useful for testing
//! testing purposes. //! purposes.
//!
//! # Examples
//! //!
//! ```rust //! ```rust
//! # #[cfg(feature = "builder")] //! # #[cfg(feature = "builder")]
@@ -12,7 +14,7 @@
//! use lettre::{transport::stub::StubTransport, Message, Transport}; //! use lettre::{transport::stub::StubTransport, Message, Transport};
//! //!
//! # use std::error::Error; //! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn try_main() -> Result<(), Box<dyn Error>> {
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
@@ -23,18 +25,36 @@
//! let mut sender = StubTransport::new_ok(); //! let mut sender = StubTransport::new_ok();
//! let result = sender.send(&email); //! let result = sender.send(&email);
//! assert!(result.is_ok()); //! assert!(result.is_ok());
//! assert_eq!(
//! sender.messages(),
//! vec![(
//! email.envelope().clone(),
//! String::from_utf8(email.formatted()).unwrap()
//! )],
//! );
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! # try_main().unwrap();
//! # } //! # }
//! ``` //! ```
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))] use std::{
error::Error as StdError,
fmt,
sync::{Arc, Mutex as StdMutex},
};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use async_trait::async_trait;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use futures_util::lock::Mutex as FuturesMutex;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport; use crate::AsyncTransport;
use crate::{address::Envelope, Transport}; use crate::{address::Envelope, Transport};
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
use async_trait::async_trait;
use std::{error::Error as StdError, fmt};
/// An error returned by the stub transport
#[non_exhaustive]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct Error; pub struct Error;
@@ -46,47 +66,113 @@ impl fmt::Display for Error {
impl StdError for Error {} impl StdError for Error {}
/// This transport logs the message envelope and returns the given response /// This transport logs messages and always returns the given response
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub struct StubTransport { pub struct StubTransport {
response: Result<(), Error>, response: Result<(), Error>,
message_log: Arc<StdMutex<Vec<(Envelope, String)>>>,
}
/// This transport logs messages and always returns the given response
#[derive(Debug, Clone)]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncStubTransport {
response: Result<(), Error>,
message_log: Arc<FuturesMutex<Vec<(Envelope, String)>>>,
} }
impl StubTransport { impl StubTransport {
/// Creates a new transport that always returns the given Result /// Creates a new transport that always returns the given Result
pub fn new(response: Result<(), Error>) -> StubTransport { pub fn new(response: Result<(), Error>) -> Self {
StubTransport { response } Self {
response,
message_log: Arc::new(StdMutex::new(vec![])),
}
} }
/// Creates a new transport that always returns a success response /// Creates a new transport that always returns a success response
pub fn new_ok() -> StubTransport { pub fn new_ok() -> Self {
StubTransport { response: Ok(()) } Self {
response: Ok(()),
message_log: Arc::new(StdMutex::new(vec![])),
}
} }
/// Creates a new transport that always returns an error /// Creates a new transport that always returns an error
pub fn new_error() -> StubTransport { pub fn new_error() -> Self {
StubTransport { Self {
response: Err(Error), response: Err(Error),
message_log: Arc::new(StdMutex::new(vec![])),
} }
} }
/// Return all logged messages sent using [`Transport::send_raw`]
pub fn messages(&self) -> Vec<(Envelope, String)> {
self.message_log
.lock()
.expect("Couldn't acquire lock to write message log")
.clone()
}
}
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl AsyncStubTransport {
/// Creates a new transport that always returns the given Result
pub fn new(response: Result<(), Error>) -> Self {
Self {
response,
message_log: Arc::new(FuturesMutex::new(vec![])),
}
}
/// Creates a new transport that always returns a success response
pub fn new_ok() -> Self {
Self {
response: Ok(()),
message_log: Arc::new(FuturesMutex::new(vec![])),
}
}
/// Creates a new transport that always returns an error
pub fn new_error() -> Self {
Self {
response: Err(Error),
message_log: Arc::new(FuturesMutex::new(vec![])),
}
}
/// Return all logged messages sent using [`AsyncTransport::send_raw`]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub async fn messages(&self) -> Vec<(Envelope, String)> {
self.message_log.lock().await.clone()
}
} }
impl Transport for StubTransport { impl Transport for StubTransport {
type Ok = (); type Ok = ();
type Error = Error; type Error = Error;
fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> { fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.message_log
.lock()
.expect("Couldn't acquire lock to write message log")
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
self.response self.response
} }
} }
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[async_trait] #[async_trait]
impl AsyncTransport for StubTransport { impl AsyncTransport for AsyncStubTransport {
type Ok = (); type Ok = ();
type Error = Error; type Error = Error;
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> { async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.message_log
.lock()
.await
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
self.response self.response
} }
} }

7
testdata/coredns.conf vendored Normal file
View File

@@ -0,0 +1,7 @@
. {
bind 127.0.0.54
forward . 9.9.9.9 8.8.8.8 1.1.1.1 {
except example.org
}
file testdata/db.example.org example.org
}

2
testdata/db.example.org vendored Normal file
View File

@@ -0,0 +1,2 @@
@ 600 IN SOA ns.example.org hostmaster.example.org 1 10800 3600 604800 3600
dkimtest._domainkey 600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+FHbM8BwkBBz/Ux5OYLQ5Bp1HVuCHTP6Rr3HXTnome/2cGl/ze0tsmmFbCjjsS89MXbMGs9xJhjv18LmL1N0UTllblOizzVjorQyN4RwBOfG34j7SS56pwzrA738Ry8FAbL5InPWEgVzbOhXuTCs8yuzcqTnm4sH/csnIl7cMWeQkVn1FR9LKMtUG0fjhDPkdX0jx3qTX1L3Z7a7gX6geY191yNd9i9DvE2/+wMigMYz1LAts4alk2g86MQhtbjc8AOR7EC15hSw37/lmamlunYLa3wC+PzHNMA8sAfnmkgNvipssjh8LnelD9qn+VtsjQB5ppkeQx3TcUPvz5z+QIDAQAB"

View File

@@ -1,13 +1,14 @@
Date: Tue, 15 Nov 1994 08:12:31 GMT Date: Tue, 15 Nov 1994 08:12:31 +0000
From: NoBody <nobody@domain.tld> From: NoBody <nobody@domain.tld>
Reply-To: Yuin <yuin@domain.tld> Reply-To: Yuin <yuin@domain.tld>
To: Hei <hei@domain.tld> To: Hei <hei@domain.tld>
Subject: Happy new year Subject: Happy new year
MIME-Version: 1.0 MIME-Version: 1.0
Content-Type: multipart/related; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1" Content-Type: multipart/related;
boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
Content-Type: text/html; charset=utf8 Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit Content-Transfer-Encoding: 7bit
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p> <p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>

View File

@@ -1,32 +1,39 @@
#[cfg(all(feature = "file-transport", feature = "builder"))]
fn default_date() -> std::time::SystemTime {
use std::time::{Duration, SystemTime};
// Tue, 15 Nov 1994 08:12:31 GMT
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151)
}
#[cfg(test)] #[cfg(test)]
#[cfg(all(feature = "file-transport", feature = "builder"))] #[cfg(all(feature = "file-transport", feature = "builder"))]
mod test { mod sync {
use lettre::{transport::file::FileTransport, Message};
use std::{ use std::{
env::temp_dir, env::temp_dir,
fs::{read_to_string, remove_file}, fs::{read_to_string, remove_file},
}; };
#[cfg(feature = "tokio02")] use lettre::{FileTransport, Message, Transport};
use tokio02_crate as tokio;
use crate::default_date;
#[test] #[test]
fn file_transport() { fn file_transport() {
use lettre::Transport;
let sender = FileTransport::new(temp_dir()); let sender = FileTransport::new(temp_dir());
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) .date(default_date())
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let result = sender.send(&email); let result = sender.send(&email);
let id = result.unwrap(); let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id)); let eml_file = temp_dir().join(format!("{id}.eml"));
let eml = read_to_string(&eml_file).unwrap(); let eml = read_to_string(&eml_file).unwrap();
assert_eq!( assert_eq!(
@@ -36,7 +43,7 @@ mod test {
"Reply-To: Yuin <yuin@domain.tld>\r\n", "Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n", "To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n", "Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Be happy!" "Be happy!"
@@ -48,24 +55,23 @@ mod test {
#[test] #[test]
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
fn file_transport_with_envelope() { fn file_transport_with_envelope() {
use lettre::Transport;
let sender = FileTransport::with_envelope(temp_dir()); let sender = FileTransport::with_envelope(temp_dir());
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) .date(default_date())
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let result = sender.send(&email); let result = sender.send(&email);
let id = result.unwrap(); let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id)); let eml_file = temp_dir().join(format!("{id}.eml"));
let eml = read_to_string(&eml_file).unwrap(); let eml = read_to_string(&eml_file).unwrap();
let json_file = temp_dir().join(format!("{}.json", id)); let json_file = temp_dir().join(format!("{id}.json"));
let json = read_to_string(&json_file).unwrap(); let json = read_to_string(&json_file).unwrap();
assert_eq!( assert_eq!(
@@ -75,7 +81,7 @@ mod test {
"Reply-To: Yuin <yuin@domain.tld>\r\n", "Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n", "To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n", "Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Be happy!" "Be happy!"
@@ -95,26 +101,88 @@ mod test {
remove_file(eml_file).unwrap(); remove_file(eml_file).unwrap();
remove_file(json_file).unwrap(); remove_file(json_file).unwrap();
} }
}
#[cfg(test)]
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))]
mod tokio_1 {
use std::{
env::temp_dir,
fs::{read_to_string, remove_file},
};
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
use tokio1_crate as tokio;
use crate::default_date;
#[tokio::test]
async fn file_transport_tokio1() {
let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
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")
.date(default_date())
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
let id = result.unwrap();
let eml_file = temp_dir().join(format!("{id}.eml"));
let eml = read_to_string(&eml_file).unwrap();
assert_eq!(
eml,
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
}
#[cfg(test)]
#[cfg(all(
feature = "file-transport",
feature = "builder",
feature = "async-std1"
))]
mod asyncstd_1 {
use std::{
env::temp_dir,
fs::{read_to_string, remove_file},
};
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
use crate::default_date;
#[cfg(feature = "async-std1")]
#[async_std::test] #[async_std::test]
async fn file_transport_asyncstd1() { async fn file_transport_asyncstd1() {
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport};
let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir()); let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap()) .date(default_date())
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let result = sender.send(email).await; let result = sender.send(email).await;
let id = result.unwrap(); let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id)); let eml_file = temp_dir().join(format!("{id}.eml"));
let eml = read_to_string(&eml_file).unwrap(); let eml = read_to_string(&eml_file).unwrap();
assert_eq!( assert_eq!(
@@ -124,44 +192,7 @@ mod test {
"Reply-To: Yuin <yuin@domain.tld>\r\n", "Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n", "To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n", "Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n", "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
#[cfg(feature = "tokio02")]
#[tokio::test]
async fn file_transport_tokio02() {
use lettre::{AsyncFileTransport, AsyncTransport, Tokio02Executor};
let sender = AsyncFileTransport::<Tokio02Executor>::new(temp_dir());
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")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id));
let eml = read_to_string(&eml_file).unwrap();
assert_eq!(
eml,
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Content-Transfer-Encoding: 7bit\r\n", "Content-Transfer-Encoding: 7bit\r\n",
"\r\n", "\r\n",
"Be happy!" "Be happy!"

View File

@@ -1,14 +1,10 @@
#[cfg(test)] #[cfg(test)]
#[cfg(all(feature = "sendmail-transport", feature = "builder"))] #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
mod test { mod sync {
use lettre::{transport::sendmail::SendmailTransport, Message}; use lettre::{Message, SendmailTransport, Transport};
#[cfg(feature = "tokio02")]
use tokio02_crate as tokio;
#[test] #[test]
fn sendmail_transport() { fn sendmail_transport() {
use lettre::Transport;
let sender = SendmailTransport::new(); let sender = SendmailTransport::new();
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
@@ -19,47 +15,60 @@ mod test {
.unwrap(); .unwrap();
let result = sender.send(&email); let result = sender.send(&email);
println!("{:?}", result); println!("{result:?}");
assert!(result.is_ok()); assert!(result.is_ok());
} }
}
#[cfg(test)]
#[cfg(all(
feature = "sendmail-transport",
feature = "builder",
feature = "tokio1"
))]
mod tokio_1 {
use lettre::{AsyncSendmailTransport, AsyncTransport, Message, Tokio1Executor};
use tokio1_crate as tokio;
#[tokio::test]
async fn sendmail_transport_tokio1() {
let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
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")
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
println!("{result:?}");
assert!(result.is_ok());
}
}
#[cfg(test)]
#[cfg(all(
feature = "sendmail-transport",
feature = "builder",
feature = "async-std1"
))]
mod asyncstd_1 {
use lettre::{AsyncSendmailTransport, AsyncStd1Executor, AsyncTransport, Message};
#[cfg(feature = "async-std1")]
#[async_std::test] #[async_std::test]
async fn sendmail_transport_asyncstd1() { async fn sendmail_transport_asyncstd1() {
use lettre::{AsyncSendmailTransport, AsyncStd1Executor, AsyncTransport};
let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new(); let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let result = sender.send(email).await; let result = sender.send(email).await;
println!("{:?}", result); println!("{result:?}");
assert!(result.is_ok());
}
#[cfg(feature = "tokio02")]
#[tokio::test]
async fn sendmail_transport_tokio02() {
use lettre::{AsyncSendmailTransport, Tokio02Executor, Tokio02Transport};
let sender = AsyncSendmailTransport::<Tokio02Executor>::new();
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")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
println!("{:?}", result);
assert!(result.is_ok()); assert!(result.is_ok());
} }
} }

View File

@@ -1,6 +1,6 @@
#[cfg(test)] #[cfg(test)]
#[cfg(all(feature = "smtp-transport", feature = "builder"))] #[cfg(all(feature = "smtp-transport", feature = "builder"))]
mod test { mod sync {
use lettre::{Message, SmtpTransport, Transport}; use lettre::{Message, SmtpTransport, Transport};
#[test] #[test]
@@ -12,10 +12,61 @@ mod test {
.subject("Happy new year") .subject("Happy new year")
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
SmtpTransport::builder_dangerous("127.0.0.1")
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525) .port(2525)
.build() .build();
.send(&email) sender.send(&email).unwrap();
}
}
#[cfg(test)]
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))]
mod tokio_1 {
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use tokio1_crate as tokio;
#[tokio::test]
async fn smtp_transport_simple_tokio1() {
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")
.body(String::from("Be happy!"))
.unwrap(); .unwrap();
let sender: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous("127.0.0.1")
.port(2525)
.build();
sender.send(email).await.unwrap();
}
}
#[cfg(test)]
#[cfg(all(
feature = "smtp-transport",
feature = "builder",
feature = "async-std1"
))]
mod asyncstd_1 {
use lettre::{AsyncSmtpTransport, AsyncStd1Executor, AsyncTransport, Message};
#[async_std::test]
async fn smtp_transport_simple_asyncstd1() {
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")
.body(String::from("Be happy!"))
.unwrap();
let sender: AsyncSmtpTransport<AsyncStd1Executor> =
AsyncSmtpTransport::<AsyncStd1Executor>::builder_dangerous("127.0.0.1")
.port(2525)
.build();
sender.send(email).await.unwrap();
} }
} }

View File

@@ -1,9 +1,9 @@
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))] #[cfg(all(test, feature = "smtp-transport", feature = "pool"))]
mod test { mod sync {
use lettre::{address::Envelope, SmtpTransport, Transport};
use std::{sync::mpsc, thread}; use std::{sync::mpsc, thread};
use lettre::{address::Envelope, SmtpTransport, Transport};
fn envelope() -> Envelope { fn envelope() -> Envelope {
Envelope::new( Envelope::new(
Some("user@localhost".parse().unwrap()), Some("user@localhost".parse().unwrap()),

View File

@@ -1,14 +1,10 @@
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
mod test { mod sync {
use lettre::{transport::stub::StubTransport, Message}; use lettre::{transport::stub::StubTransport, Message, Transport};
#[cfg(feature = "tokio02")]
use tokio02_crate as tokio;
#[test] #[test]
fn stub_transport() { fn stub_transport() {
use lettre::Transport;
let sender_ok = StubTransport::new_ok(); let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error(); let sender_ko = StubTransport::new_error();
let email = Message::builder() let email = Message::builder()
@@ -21,45 +17,68 @@ mod test {
sender_ok.send(&email).unwrap(); sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err(); sender_ko.send(&email).unwrap_err();
let expected_messages = [(
email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(),
)];
assert_eq!(sender_ok.messages(), expected_messages);
}
} }
#[cfg(feature = "async-std1")] #[cfg(test)]
#[cfg(all(feature = "builder", feature = "tokio1"))]
mod tokio_1 {
use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
use tokio1_crate as tokio;
#[tokio::test]
async fn stub_transport_tokio1() {
let sender_ok = AsyncStubTransport::new_ok();
let sender_ko = AsyncStubTransport::new_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")
.body(String::from("Be happy!"))
.unwrap();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email.clone()).await.unwrap_err();
let expected_messages = [(
email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(),
)];
assert_eq!(sender_ok.messages().await, expected_messages);
}
}
#[cfg(test)]
#[cfg(all(feature = "builder", feature = "async-std1"))]
mod asyncstd_1 {
use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
#[async_std::test] #[async_std::test]
async fn stub_transport_asyncstd1() { async fn stub_transport_asyncstd1() {
use lettre::AsyncStd1Transport; let sender_ok = AsyncStubTransport::new_ok();
let sender_ko = AsyncStubTransport::new_error();
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
sender_ok.send(email.clone()).await.unwrap(); sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err(); sender_ko.send(email.clone()).await.unwrap_err();
}
#[cfg(feature = "tokio02")] let expected_messages = [(
#[tokio::test] email.envelope().clone(),
async fn stub_transport_tokio02() { String::from_utf8(email.formatted()).unwrap(),
use lettre::Tokio02Transport; )];
assert_eq!(sender_ok.messages().await, expected_messages);
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_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")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err();
} }
} }