1#![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}