ceal/lib.rs
1//! Opportunistic Encryption for E-Mail
2//!
3//! This library can query the S/MIME and OpenPGP keys from the recipient's DNS record
4//! and use it to encrypt messages. The keys are stored in the [SMIMEA] or [OPENPGPKEY]
5//! records respectively. All recipients must have at least one of the same record.
6//! Unfortunately, E-Mail programs are not very good when presented with a choice between
7//! two different encryption methods within the same message. If encryption is not
8//! possible due to lack of keys (of the same type), the message is sent unencrypted.
9//!
10//! You can use this as a stand-alone library or as an extension to [`lettre`].
11//!
12//! ## Example Usage: Stand-Alone library
13//!
14//! You can use this library by providing raw messages to it, like this:
15//!
16//! ```
17//! use email_address::EmailAddress;
18#![cfg_attr(feature = "tokio", doc = "# #[tokio::main]")]
19//! # async fn main() {
20//! let message = b"From: John Doe <john.doe@example.org>\r
21//! To: Max Mustermann <max.mustermann@example.org>\r
22//! Subject: Hello\r
23//! Content-Type: text/plain; charset=\"utf-8\"\r
24//! Content-Transfer-Encoding: 7bit\r
25//! \r
26//! Hello, how are you?\r
27//! ";
28//!
29//! let recipients = ["max.mustermann@example.org".parse::<EmailAddress>().unwrap()];
30//! let message = ceal::encrypt(message, &recipients).await;
31//! # }
32//! ```
33//!
34//! If an encryption key was found, the result will look something like this:
35//! ```eml
36//! From: John Doe <john.doe@example.org>
37//! To: Max Mustermann <max.mustermann@example.org>
38//! Subject: Hello
39//! Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
40//! boundary="=-fRbE3wtPT1u0rEVI"
41//!
42//! --=-fRbE3wtPT1u0rEVI
43//! Content-Type: application/pgp-encrypted
44//! Content-Transfer-Encoding: 7bit
45//!
46//! Version: 1
47//!
48//! --=-fRbE3wtPT1u0rEVI
49//! Content-Type: application/octet-stream; name="encrypted.asc"
50//! Content-Description: OpenPGP Encrypted Message
51//! Content-Transfer-Encoding: 7bit
52//!
53//! -----BEGIN PGP MESSAGE-----
54//!
55//! wWwGFQQAaKICkqGFdPWZFLNGsneBbdOVVBIBB0DS2mtKK0qe7UPwhrxFKUFzywC6
56//! yaXOKdNnIb1fHPM7eDBttgL9Injkz237nD98dwvdF4RD3dLbLGoQtw9nufbgyihf
57//! mRr7zUVHdZf8hjpkRVfSrwIJAgaO3vLYxhj3lMiOCgDda12W0bJJl9clRwXEOnlh
58//! m1duZv2Wxlx+ig1WRJuSYbKIUKg9MrX1mqsVdlF5DdNkEH2ywCmI1UTXBYzSKQEv
59//! 1h/+77iozcYqiJfV74TuWM9AT3YrNPA0SZnFFkttXV0vhsuvi72DbTTq9sieqH13
60//! IH9FiCin5kefw5y4/MeIFqJXbfKzgP+8z4tXYwAQmrqskT662PndfvzbJSJ+4LQ=
61//! -----END PGP MESSAGE-----
62//!
63//! --=-fRbE3wtPT1u0rEVI--
64//! ```
65//!
66//! ## Example Usage: [`lettre`] transport
67//!
68//! First, you need to enable the `lettre` feature of this crate. Then you may use it
69//! to create a [`lettre`] transport that applies opportunistic encryption just before
70//! sending the message:
71//! ```
72//! use lettre::{
73//! AsyncTransport, Message, SmtpTransport, Tokio1Executor,
74//! message::SinglePart,
75//! transport::smtp::{AsyncSmtpTransport, client::{Tls, TlsParameters}}
76//! };
77//! use ceal::lettre::AsyncTransportExt as _;
78//!
79//! # async fn async_main() {
80//! let email = Message::builder()
81//! .from("John Doe <john.doe@example.org>".parse().unwrap())
82//! .to("Max Mustermann <max.mustermann@example.org>".parse().unwrap())
83//! .subject("Hello")
84//! .singlepart(SinglePart::plain(String::from("Hello, how are you?")))
85//! .unwrap();
86//!
87//! let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay("mail.example.org")
88//! .unwrap()
89//! .tls(Tls::Required(TlsParameters::new("mail.example.org".into()).unwrap()))
90//! .build()
91//! .with_opportunistic_encryption();
92//!
93//! mailer.send(email).await.expect("Could not send E-Mail");
94//! # }
95//! ```
96//!
97//!
98//! [SMIMEA]: https://datatracker.ietf.org/doc/html/rfc8162#section-2
99//! [OPENPGPKEY]: https://datatracker.ietf.org/doc/html/rfc7929#section-2
100
101pub mod address;
102pub mod dns;
103#[cfg(feature = "lettre")]
104pub mod lettre;
105mod message;
106pub mod openpgp;
107pub mod smime;
108
109use self::address::Address;
110use openssl::x509::X509;
111use std::borrow::Cow;
112
113#[cfg(not(any(feature = "tokio")))]
114compile_error!(
115 "No runtime provider enabled. You must enable one of the following features: tokio"
116);
117
118#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
119compile_error!(
120 "No crypto provider enabled. You must enable one of the following features: aws-lc-rs, ring"
121);
122
123/// Apply opportunistic encryption to this message.
124///
125/// This will query the DANE DNS records (SMIMEA and OPENPGPKEY) for each recipient
126/// of this message. If all recipients have at least one record of the same type,
127/// the message is encrypted for all recipients.
128///
129/// The chance of finding suitable keys is thus increased the less recipients there
130/// are. It is therefore advisable to create one message per recipient.
131///
132/// If encryption fails, the message will be returned unencrypted.
133pub async fn encrypt<'m, A: Address>(msg: &'m [u8], recipients: &[A]) -> Cow<'m, [u8]> {
134 if let Ok(smime_certs) = smime::find_certs_for_all_recipients(recipients).await {
135 return encrypt_smime(msg, smime_certs);
136 }
137
138 if let Ok(openpgp_certs) = openpgp::find_certs_for_all_recipients(recipients).await {
139 return encrypt_openpgp(msg, openpgp_certs);
140 }
141
142 msg.into()
143}
144
145/// Encrypt the message using the provided S/MIME certificates.
146pub fn encrypt_smime(msg: &[u8], recipient_certs: Vec<X509>) -> Cow<'_, [u8]> {
147 smime::encrypt(msg, recipient_certs)
148}
149
150/// Encrypt the message using the provided OpenPGP certificates.
151pub fn encrypt_openpgp(
152 msg: &[u8],
153 recipient_certs: Vec<sequoia_openpgp::Cert>
154) -> Cow<'_, [u8]> {
155 openpgp::encrypt(msg, recipient_certs)
156}