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//! [![crates.io](https://img.shields.io/crates/v/mail-send)](https://crates.io/crates/mail-send)
14//! [![build](https://github.com/stalwartlabs/mail-send/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/mail-send/actions/workflows/rust.yml)
15//! [![docs.rs](https://img.shields.io/docsrs/mail-send)](https://docs.rs/mail-send)
16//! [![crates.io](https://img.shields.io/crates/l/mail-send)](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}