use std::sync::Arc;
use crate::rediscache::RedisPool;
use crate::response::error::{AppError, AppResult};
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
#[derive(Debug, Clone)]
pub struct EmailConfig {
pub debug: bool,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_username: Option<String>,
pub smtp_password: Option<String>,
pub from_email: String,
pub from_name: Option<String>,
pub subject_prefix: Option<String>,
}
#[derive(Debug, Clone)]
pub struct EmailSendResult {
pub provider: &'static str,
pub message_id: Option<String>,
}
pub struct EmailService;
impl EmailService {
pub async fn send_captcha(
config: &Arc<EmailConfig>,
redis_pool: &Arc<RedisPool>,
email: &str,
redis_key_prefix: &str,
email_regex: ®ex::Regex,
) -> AppResult<()> {
Self::send_captcha_with_options(
config,
redis_pool,
email,
redis_key_prefix,
email_regex,
60 * 5,
)
.await
.map(|_| ())
}
pub async fn send_captcha_with_options(
config: &Arc<EmailConfig>,
redis_pool: &Arc<RedisPool>,
email: &str,
redis_key_prefix: &str,
email_regex: ®ex::Regex,
expire_seconds: u64,
) -> AppResult<EmailSendResult> {
if !email_regex.is_match(email) {
return Err(AppError::ClientError("邮箱格式不正确".to_string()));
}
let code_num: u32 = rand::random::<u32>() % 900000 + 100000;
let code = code_num.to_string();
if config.debug {
Self::store_captcha_code_with_options(
redis_pool,
email,
code_num,
expire_seconds,
redis_key_prefix,
)
.await?;
return Ok(EmailSendResult {
provider: "debug",
message_id: None,
});
}
let from_name = config
.from_name
.clone()
.unwrap_or_else(|| "StackLoom".to_string());
let subject_prefix = config
.subject_prefix
.clone()
.unwrap_or_else(|| "[StackLoom]".to_string());
let subject = format!("{subject_prefix} Password reset verification code");
let body = format!(
"<div><p>Your verification code is <b>{code}</b>.</p><p>This code expires in 5 minutes.</p></div>"
);
let from = format!("{from_name} <{}>", config.from_email)
.parse()
.map_err(|err| AppError::ClientError(format!("invalid from email config: {err}")))?;
let to = email
.parse()
.map_err(|err| AppError::ClientError(format!("invalid email address: {err}")))?;
let message = Message::builder()
.from(from)
.to(to)
.subject(subject)
.header(ContentType::TEXT_HTML)
.body(body)
.map_err(|err| AppError::ClientError(format!("failed to build email: {err}")))?;
let mut mailer_builder =
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(config.smtp_host.as_str())
.map_err(|err| AppError::ClientError(format!("invalid smtp host: {err}")))?;
mailer_builder = mailer_builder.port(config.smtp_port);
if let (Some(username), Some(password)) =
(config.smtp_username.as_ref(), config.smtp_password.as_ref())
{
mailer_builder =
mailer_builder.credentials(Credentials::new(username.clone(), password.clone()));
}
let mailer = mailer_builder.build();
let response = mailer
.send(message)
.await
.map_err(|err| AppError::ClientError(format!("email send failed: {err}")))?;
Self::store_captcha_code_with_options(
redis_pool,
email,
code_num,
expire_seconds,
redis_key_prefix,
)
.await?;
Ok(EmailSendResult {
provider: "smtp",
message_id: {
let parts = response
.message()
.map(|value| value.to_string())
.collect::<Vec<_>>();
if parts.is_empty() {
None
} else {
Some(parts.join("; "))
}
},
})
}
pub async fn valid_auth_captcha(
redis_pool: &Arc<RedisPool>,
email: &str,
captcha: &str,
redis_key_prefix: &str,
delete: bool,
) -> AppResult<()> {
let code = Self::get_captcha_code(redis_pool, email, redis_key_prefix).await?;
match code {
Some(code) => {
if code != captcha {
Self::delete_captcha_code(redis_pool, email, redis_key_prefix).await?;
Err(AppError::ClientError("验证码错误".to_string()))
} else {
if delete {
Self::delete_captcha_code(redis_pool, email, redis_key_prefix).await?;
}
Ok(())
}
}
None => Err(AppError::ClientError("验证码已过期".to_string())),
}
}
pub async fn store_captcha_code_with_options(
redis_pool: &Arc<RedisPool>,
email: &str,
code: u32,
expire_seconds: u64,
key_prefix: &str,
) -> AppResult<()> {
let key = format!("{}{}", key_prefix, email);
let value = code.to_string();
redis_pool
.setex(&key, &value, expire_seconds)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
Ok(())
}
pub async fn get_captcha_code(
redis_pool: &Arc<RedisPool>,
email: &str,
redis_key_prefix: &str,
) -> AppResult<Option<String>> {
let key = format!("{}{}", redis_key_prefix, email);
match redis_pool.get(&key).await {
Ok(Some(value)) => Ok(Some(value)),
Ok(None) => Ok(None),
Err(e) => Err(AppError::RedisError(e.to_string())),
}
}
pub async fn delete_captcha_code(
redis_pool: &Arc<RedisPool>,
email: &str,
redis_key_prefix: &str,
) -> AppResult<()> {
let key = format!("{}{}", redis_key_prefix, email);
redis_pool
.del(&key)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
Ok(())
}
}