neocrates 0.1.55

A comprehensive Rust library for various utilities and helpers
Documentation
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: &regex::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: &regex::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(())
    }
}