spring_mail/
lib.rs

1//! [![spring-rs](https://img.shields.io/github/stars/spring-rs/spring-rs)](https://spring-rs.github.io/docs/plugins/spring-mail)
2#![doc(html_favicon_url = "https://spring-rs.github.io/favicon.ico")]
3#![doc(html_logo_url = "https://spring-rs.github.io/logo.svg")]
4
5pub mod config;
6
7pub use lettre::message::*;
8pub use lettre::transport::smtp::response::Response;
9pub use lettre::AsyncTransport;
10pub use lettre::Message;
11
12use anyhow::Context;
13use config::MailerConfig;
14use config::SmtpTransportConfig;
15use lettre::address::Envelope;
16use lettre::transport::smtp::response::Category;
17use lettre::transport::smtp::response::Code;
18use lettre::transport::smtp::response::Detail;
19use lettre::transport::smtp::response::Severity;
20use lettre::{transport::smtp::authentication::Credentials, Tokio1Executor};
21use spring::async_trait;
22use spring::config::ConfigRegistry;
23use spring::plugin::MutableComponentRegistry;
24use spring::{app::AppBuilder, error::Result, plugin::Plugin};
25
26pub type TokioMailerTransport = lettre::AsyncSmtpTransport<Tokio1Executor>;
27pub type StubMailerTransport = lettre::transport::stub::AsyncStubTransport;
28
29#[derive(Clone)]
30pub enum Mailer {
31    Tokio(TokioMailerTransport),
32    Stub(StubMailerTransport),
33}
34
35#[async_trait]
36impl AsyncTransport for Mailer {
37    type Ok = Response;
38    type Error = spring::error::AppError;
39
40    async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok> {
41        Ok(match self {
42            Self::Tokio(tokio_transport) => tokio_transport
43                .send_raw(envelope, email)
44                .await
45                .context("mailer send failed")?,
46            Self::Stub(stub_transport) => {
47                stub_transport
48                    .send_raw(envelope, email)
49                    .await
50                    .context("stub mailer send failed")?;
51                Response::new(
52                    Code {
53                        severity: Severity::PositiveCompletion,
54                        category: Category::MailSystem,
55                        detail: Detail::Zero,
56                    },
57                    vec!["stub mailer send success".to_string()],
58                )
59            }
60        })
61    }
62}
63
64pub struct MailPlugin;
65
66#[async_trait]
67impl Plugin for MailPlugin {
68    async fn build(&self, app: &mut AppBuilder) {
69        let config = app
70            .get_config::<MailerConfig>()
71            .expect("mail plugin config load failed");
72
73        let mailer = if config.stub {
74            Mailer::Stub(StubMailerTransport::new_ok())
75        } else {
76            let sender = if let Some(uri) = config.uri {
77                TokioMailerTransport::from_url(&uri)
78                    .expect("build mail plugin failed")
79                    .build()
80            } else if let Some(transport) = config.transport {
81                Self::build_smtp_transport(&transport).expect("build mail plugin failed")
82            } else {
83                panic!("The mail plugin is missing necessary smtp transport configuration");
84            };
85            if config.test_connection
86                && !sender
87                    .test_connection()
88                    .await
89                    .expect("test mail connection failed")
90            {
91                panic!("Unable to connect to the mail server");
92            }
93            Mailer::Tokio(sender)
94        };
95
96        app.add_component(mailer);
97    }
98}
99
100impl MailPlugin {
101    fn build_smtp_transport(config: &SmtpTransportConfig) -> Result<TokioMailerTransport> {
102        let mut transport_builder = if config.secure {
103            TokioMailerTransport::relay(&config.host)
104                .with_context(|| format!("build mailer failed: {}", config.host))?
105                .port(config.port)
106        } else {
107            TokioMailerTransport::builder_dangerous(&config.host).port(config.port)
108        };
109
110        if let Some(auth) = config.auth.as_ref() {
111            let credentials = Credentials::new(auth.user.clone(), auth.password.clone());
112            transport_builder = transport_builder.credentials(credentials);
113        }
114
115        Ok(transport_builder.build())
116    }
117}