mail_send/lib.rs
1/*
2 * Copyright Stalwart Labs Ltd.
3 *
4 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
7 * option. This file may not be copied, modified, or distributed
8 * except according to those terms.
9 */
10
11//! # mail-send
12//!
13//! [](https://crates.io/crates/mail-send)
14//! [](https://github.com/stalwartlabs/mail-send/actions/workflows/rust.yml)
15//! [](https://docs.rs/mail-send)
16//! [](http://www.apache.org/licenses/LICENSE-2.0)
17//!
18//! _mail-send_ is a Rust library to build, sign and send e-mail messages via SMTP. It includes the following features:
19//!
20//! - Generates **e-mail** messages conforming to the Internet Message Format standard (_RFC 5322_).
21//! - Full **MIME** support (_RFC 2045 - 2049_) with automatic selection of the most optimal encoding for each message body part.
22//! - DomainKeys Identified Mail (**DKIM**) Signatures (_RFC 6376_) with ED25519-SHA256, RSA-SHA256 and RSA-SHA1 support.
23//! - Simple Mail Transfer Protocol (**SMTP**; _RFC 5321_) delivery.
24//! - SMTP Service Extension for Secure SMTP over **TLS** (_RFC 3207_).
25//! - SMTP Service Extension for Authentication (_RFC 4954_) with automatic mechanism negotiation (from most secure to least secure):
26//! - CRAM-MD5 (_RFC 2195_)
27//! - DIGEST-MD5 (_RFC 2831_; obsolete but still supported)
28//! - XOAUTH2 (Google proprietary)
29//! - LOGIN
30//! - PLAIN
31//! - Full async (requires Tokio).
32//!
33//! ## Usage Example
34//!
35//! Send a message via an SMTP server that requires authentication:
36//!
37//! ```rust
38//! // Build a simple multipart message
39//! let message = MessageBuilder::new()
40//! .from(("John Doe", "john@example.com"))
41//! .to(vec![
42//! ("Jane Doe", "jane@example.com"),
43//! ("James Smith", "james@test.com"),
44//! ])
45//! .subject("Hi!")
46//! .html_body("<h1>Hello, world!</h1>")
47//! .text_body("Hello world!");
48//!
49//! // Connect to the SMTP submissions port, upgrade to TLS and
50//! // authenticate using the provided credentials.
51//! SmtpClientBuilder::new("smtp.gmail.com", 587)
52//! .implicit_tls(false)
53//! .credentials(("john", "p4ssw0rd"))
54//! .connect()
55//! .await
56//! .unwrap()
57//! .send(message)
58//! .await
59//! .unwrap();
60//! ```
61//!
62//! Sign a message with DKIM and send it via an SMTP relay server:
63//!
64//! ```rust
65//! // Build a simple text message with a single attachment
66//! let message = MessageBuilder::new()
67//! .from(("John Doe", "john@example.com"))
68//! .to("jane@example.com")
69//! .subject("Howdy!")
70//! .text_body("These pretzels are making me thirsty.")
71//! .attachment("image/png", "pretzels.png", [1, 2, 3, 4].as_ref());
72//!
73//! // Sign an e-mail message using RSA-SHA256
74//! let pk_rsa = RsaKey::<Sha256>::from_rsa_pem(TEST_KEY).unwrap();
75//! let signer = DkimSigner::from_key(pk_rsa)
76//! .domain("example.com")
77//! .selector("default")
78//! .headers(["From", "To", "Subject"])
79//! .expiration(60 * 60 * 7); // Number of seconds before this signature expires (optional)
80//!
81//! // Connect to an SMTP relay server over TLS.
82//! // Signs each message with the configured DKIM signer.
83//! SmtpClientBuilder::new("smtp.gmail.com", 465)
84//! .connect()
85//! .await
86//! .unwrap()
87//! .send_signed(message, &signer)
88//! .await
89//! .unwrap();
90//! ```
91//!
92//! More examples of how to build messages are available in the [`mail-builder`](https://crates.io/crates/mail-builder) crate.
93//! Please note that this library does not support parsing e-mail messages as this functionality is provided separately by the [`mail-parser`](https://crates.io/crates/mail-parser) crate.
94//!
95//! ## Testing
96//!
97//! To run the testsuite:
98//!
99//! ```bash
100//! $ cargo test --all-features
101//! ```
102//!
103//! or, to run the testsuite with MIRI:
104//!
105//! ```bash
106//! $ cargo +nightly miri test --all-features
107//! ```
108//!
109//! ## License
110//!
111//! Licensed under either of
112//!
113//! * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
114//! * MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
115//!
116//! at your option.
117//!
118//! ## Copyright
119//!
120//! Copyright (C) 2020-2022, Stalwart Labs Ltd.
121//!
122//! See [COPYING] for the license.
123//!
124//! [COPYING]: https://github.com/stalwartlabs/mail-send/blob/main/COPYING
125//!
126
127pub mod smtp;
128use std::{fmt::Display, hash::Hash, time::Duration};
129use tokio::io::{AsyncRead, AsyncWrite};
130use tokio_rustls::TlsConnector;
131
132#[cfg(feature = "builder")]
133pub use mail_builder;
134
135#[cfg(feature = "dkim")]
136pub use mail_auth;
137
138#[derive(Debug)]
139pub enum Error {
140 /// I/O error
141 Io(std::io::Error),
142
143 /// TLS error
144 Tls(Box<rustls::Error>),
145
146 /// Base64 decode error
147 Base64(base64::DecodeError),
148
149 // SMTP authentication error.
150 Auth(smtp::auth::Error),
151
152 /// Failure parsing SMTP reply
153 UnparseableReply,
154
155 /// Unexpected SMTP reply.
156 UnexpectedReply(smtp_proto::Response<String>),
157
158 /// SMTP authentication failure.
159 AuthenticationFailed(smtp_proto::Response<String>),
160
161 /// Invalid TLS name provided.
162 InvalidTLSName,
163
164 /// Missing authentication credentials.
165 MissingCredentials,
166
167 /// Missing message sender.
168 MissingMailFrom,
169
170 /// Missing message recipients.
171 MissingRcptTo,
172
173 /// The server does no support any of the available authentication methods.
174 UnsupportedAuthMechanism,
175
176 /// Connection timeout.
177 Timeout,
178
179 /// STARTTLS not available
180 MissingStartTls,
181}
182
183impl std::error::Error for Error {
184 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
185 match self {
186 Error::Io(ref err) => err.source(),
187 Error::Tls(ref err) => err.source(),
188 Error::Base64(ref err) => err.source(),
189 _ => None,
190 }
191 }
192}
193
194pub type Result<T> = std::result::Result<T, Error>;
195
196/// SMTP client builder
197#[derive(Clone)]
198pub struct SmtpClientBuilder<T: AsRef<str> + PartialEq + Eq + Hash> {
199 pub timeout: Duration,
200 pub tls_connector: TlsConnector,
201 pub tls_hostname: T,
202 pub tls_implicit: bool,
203 pub credentials: Option<Credentials<T>>,
204 pub addr: String,
205 pub is_lmtp: bool,
206 pub say_ehlo: bool,
207 pub local_host: String,
208}
209
210/// SMTP client builder
211pub struct SmtpClient<T: AsyncRead + AsyncWrite> {
212 pub stream: T,
213 pub timeout: Duration,
214}
215
216#[derive(Clone, PartialEq, Eq, Hash)]
217pub enum Credentials<T: AsRef<str> + PartialEq + Eq + Hash> {
218 Plain { username: T, secret: T },
219 OAuthBearer { token: T },
220 XOauth2 { username: T, secret: T },
221}
222
223impl Default for Credentials<String> {
224 fn default() -> Self {
225 Credentials::Plain {
226 username: String::new(),
227 secret: String::new(),
228 }
229 }
230}
231
232impl Display for Error {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 match self {
235 Error::Io(e) => write!(f, "I/O error: {e}"),
236 Error::Tls(e) => write!(f, "TLS error: {e}"),
237 Error::Base64(e) => write!(f, "Base64 decode error: {e}"),
238 Error::Auth(e) => write!(f, "SMTP authentication error: {e}"),
239 Error::UnparseableReply => write!(f, "Unparseable SMTP reply"),
240 Error::UnexpectedReply(e) => write!(f, "Unexpected reply: {e}"),
241 Error::AuthenticationFailed(e) => write!(f, "Authentication failed: {e}"),
242 Error::InvalidTLSName => write!(f, "Invalid TLS name provided"),
243 Error::MissingCredentials => write!(f, "Missing authentication credentials"),
244 Error::MissingMailFrom => write!(f, "Missing message sender"),
245 Error::MissingRcptTo => write!(f, "Missing message recipients"),
246 Error::UnsupportedAuthMechanism => write!(
247 f,
248 "The server does no support any of the available authentication methods"
249 ),
250 Error::Timeout => write!(f, "Connection timeout"),
251 Error::MissingStartTls => write!(f, "STARTTLS extension unavailable"),
252 }
253 }
254}
255
256impl From<std::io::Error> for Error {
257 fn from(err: std::io::Error) -> Self {
258 Error::Io(err)
259 }
260}
261
262impl From<base64::DecodeError> for Error {
263 fn from(err: base64::DecodeError) -> Self {
264 Error::Base64(err)
265 }
266}