From 3988be47d5a9df76195172225f2e8c0655211883 Mon Sep 17 00:00:00 2001 From: Meng Zhang Date: Sat, 10 Feb 2024 00:16:56 -0800 Subject: [PATCH] refactor(email): switch email send to backend (#1433) --- Cargo.lock | 3 ++ ee/tabby-webserver/Cargo.toml | 4 +- ee/tabby-webserver/src/schema/email.rs | 15 +----- ee/tabby-webserver/src/schema/mod.rs | 2 +- ee/tabby-webserver/src/service/email.rs | 68 ++++++++++++++----------- 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f0faf417267..134781742b67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1992,11 +1992,13 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5aaf628956b6b0852e12ac3505d20d7a12ecc1e32d5ea921f002af4a74036a5" dependencies = [ + "async-trait", "base64 0.21.5", "chumsky", "email-encoding", "email_address", "fastrand 2.0.1", + "futures-io", "futures-util", "hostname", "httpdate", @@ -2007,6 +2009,7 @@ dependencies = [ "quoted_printable", "socket2 0.5.5", "tokio", + "tokio-native-tls", "url", ] diff --git a/ee/tabby-webserver/Cargo.toml b/ee/tabby-webserver/Cargo.toml index 124427fe4e58..9219c4ca95d8 100644 --- a/ee/tabby-webserver/Cargo.toml +++ b/ee/tabby-webserver/Cargo.toml @@ -17,12 +17,12 @@ bincode = "1.3.3" chrono = { workspace = true, features = ["serde"] } futures.workspace = true hash-ids = "0.2.1" -hyper = { workspace = true, features=["client"]} +hyper = { workspace = true, features = ["client"] } jsonwebtoken = "9.1.0" juniper.workspace = true juniper-axum = { path = "../../crates/juniper-axum" } lazy_static.workspace = true -lettre = "0.11.3" +lettre = { version = "0.11.3", features = ["tokio1", "tokio1-native-tls"] } mime_guess = "2.0.4" pin-project = "1.1.3" querystring = "1.1.0" diff --git a/ee/tabby-webserver/src/schema/email.rs b/ee/tabby-webserver/src/schema/email.rs index 5eecad3d8386..a5d85406be27 100644 --- a/ee/tabby-webserver/src/schema/email.rs +++ b/ee/tabby-webserver/src/schema/email.rs @@ -39,24 +39,11 @@ pub struct EmailSettingInput { pub smtp_password: Option, } -#[derive(thiserror::Error, Debug)] -pub enum SendEmailError { - #[error("Email is not enabled")] - NotEnabled, - - #[error(transparent)] - Other(#[from] anyhow::Error), -} - #[async_trait] pub trait EmailService: Send + Sync { async fn get_email_setting(&self) -> Result>; async fn update_email_setting(&self, input: EmailSettingInput) -> Result<()>; async fn delete_email_setting(&self) -> Result<()>; - async fn send_invitation_email( - &self, - email: String, - code: String, - ) -> Result<(), SendEmailError>; + async fn send_invitation_email(&self, email: String, code: String) -> Result<()>; } diff --git a/ee/tabby-webserver/src/schema/mod.rs b/ee/tabby-webserver/src/schema/mod.rs index 7a0fb146b943..322dd9d2a239 100644 --- a/ee/tabby-webserver/src/schema/mod.rs +++ b/ee/tabby-webserver/src/schema/mod.rs @@ -396,7 +396,7 @@ impl Mutation { .send_invitation_email(email, invitation.code) .await; match email_sent { - Ok(_) | Err(email::SendEmailError::NotEnabled) => {} + Ok(_) => {} Err(e) => { warn!( "Failed to send invitation email, please check your SMTP settings are correct: {e}" diff --git a/ee/tabby-webserver/src/service/email.rs b/ee/tabby-webserver/src/service/email.rs index 13b9a671f641..b307c23f9b51 100644 --- a/ee/tabby-webserver/src/service/email.rs +++ b/ee/tabby-webserver/src/service/email.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use anyhow::{anyhow, Result}; use async_trait::async_trait; use lettre::{ @@ -5,24 +7,22 @@ use lettre::{ transport::smtp::{ authentication::{Credentials, Mechanism}, client::{Tls, TlsParameters}, - SmtpTransportBuilder, + AsyncSmtpTransportBuilder, }, - Address, SmtpTransport, Transport, + Address, AsyncSmtpTransport, AsyncTransport, Tokio1Executor, Transport, }; use tabby_db::{DbConn, DbEnum}; use tokio::sync::RwLock; use tracing::warn; use crate::schema::{ - email::{ - AuthMethod, EmailService, EmailSetting, EmailSettingInput, Encryption, SendEmailError, - }, + email::{AuthMethod, EmailService, EmailSetting, EmailSettingInput, Encryption}, setting::SettingService, }; struct EmailServiceImpl { db: DbConn, - smtp_server: RwLock>, + smtp_server: Arc>>>, from: RwLock, } @@ -38,17 +38,19 @@ fn make_smtp_builder( host: &str, port: u16, encryption: Encryption, -) -> Result { +) -> Result { let tls_parameters = TlsParameters::new(host.into())?; let builder = match encryption { - Encryption::StartTls => SmtpTransport::builder_dangerous(host) + Encryption::StartTls => AsyncSmtpTransport::::builder_dangerous(host) .port(port) .tls(Tls::Required(tls_parameters)), - Encryption::SslTls => SmtpTransport::builder_dangerous(host) + Encryption::SslTls => AsyncSmtpTransport::::builder_dangerous(host) .port(port) .tls(Tls::Wrapper(tls_parameters)), - Encryption::None => SmtpTransport::builder_dangerous(host).port(port), + Encryption::None => { + AsyncSmtpTransport::::builder_dangerous(host).port(port) + } }; Ok(builder) @@ -62,6 +64,7 @@ impl EmailServiceImpl { password: String, host: &str, port: i32, + from_address: &str, encryption: Encryption, auth_method: AuthMethod, ) -> Result<()> { @@ -72,6 +75,7 @@ impl EmailServiceImpl { .authentication(auth_mechanism(auth_method)) .build(), ); + *self.from.write().await = from_address.into(); Ok(()) } @@ -80,26 +84,36 @@ impl EmailServiceImpl { *self.smtp_server.write().await = None; } - async fn send_mail( + async fn send_mail_in_background( &self, to: String, subject: String, message: String, - ) -> Result<(), SendEmailError> { - let smtp_server = self.smtp_server.read().await; - let Some(smtp_server) = &*smtp_server else { - return Err(SendEmailError::NotEnabled); - }; - let from = self.from.read().await; - let address_from = to_address(from.clone())?; + ) -> Result<()> { + let smtp_server = self.smtp_server.clone(); + let from = self.from.read().await.clone(); + let address_from = to_address(from)?; let address_to = to_address(to)?; let msg = MessageBuilder::new() .subject(subject) - .from(Mailbox::new(None, address_from)) + .from(Mailbox::new(Some("Tabby Server".to_owned()), address_from)) .to(Mailbox::new(None, address_to)) .body(message) .map_err(anyhow::Error::msg)?; - smtp_server.send(&msg).map_err(anyhow::Error::msg)?; + + tokio::spawn(async move { + let Some(smtp_server) = &*(smtp_server.read().await) else { + // Not enabled. + return; + }; + match smtp_server.send(msg).await.map_err(anyhow::Error::msg) { + Ok(_) => {} + Err(err) => { + warn!("Failed to send mail due to {}", err); + } + }; + }); + Ok(()) } } @@ -121,6 +135,7 @@ pub async fn new_email_service(db: DbConn) -> Result { setting.smtp_password, &setting.smtp_server, setting.smtp_port as i32, + &setting.from_address, encryption, auth_method, ) @@ -157,7 +172,6 @@ impl EmailService for EmailServiceImpl { input.auth_method.as_enum_str().into(), ) .await?; - *self.from.write().await = input.smtp_username.clone(); let smtp_password = match input.smtp_password { Some(pass) => pass, None => { @@ -174,6 +188,7 @@ impl EmailService for EmailServiceImpl { smtp_password.clone(), &input.smtp_server, input.smtp_port, + &input.from_address, input.encryption, input.auth_method, ) @@ -188,18 +203,13 @@ impl EmailService for EmailServiceImpl { Ok(()) } - async fn send_invitation_email( - &self, - email: String, - code: String, - ) -> Result<(), SendEmailError> { + async fn send_invitation_email(&self, email: String, code: String) -> Result<()> { let network_setting = self.db.read_network_setting().await?; let external_url = network_setting.external_url; - self.send_mail( + self.send_mail_in_background( email, "You've been invited to join a Tabby workspace!".into(), - format!("Welcome to Tabby! You have been invited to join a Tabby instance, where you can tap into\ - AI-driven code completions and chat assistants. Your invite code is {code}, go to {external_url}/auth/signup?invitationCode={code} to join!"), + format!("Welcome to Tabby! You have been invited to join a Tabby Server, where you can tap into AI-driven code completions and chat assistants.\n\nGo to {external_url}/auth/signup?invitationCode={code} to join!"), ).await } }