use std::sync::Arc;
use crate::errors::AppError;
use crate::repositories::{OutboxEvent, OutboxEventType};
use crate::services::{
AccountDeletionEmailData, EmailService, InstantLinkEmailData, InviteEmailData,
PasswordResetEmailData, SecurityAlertEmailData, SettingsService, VerificationEmailData,
};
use crate::utils::TokenCipher;
async fn get_custom_subject(settings: &Option<Arc<SettingsService>>, key: &str) -> Option<String> {
let ss = settings.as_ref()?;
match ss.get(key).await {
Ok(Some(v)) if !v.is_empty() => Some(v),
_ => None,
}
}
pub async fn process_email_event(
event: &OutboxEvent,
email_service: &dyn EmailService,
base_url: &str,
token_cipher: &TokenCipher,
settings: Option<Arc<SettingsService>>,
) -> Result<(), AppError> {
match event.event_type {
OutboxEventType::EmailVerification => {
process_verification_email(event, email_service, base_url, token_cipher, &settings)
.await
}
OutboxEventType::EmailPasswordReset => {
process_password_reset_email(event, email_service, base_url, token_cipher, &settings)
.await
}
OutboxEventType::EmailInvite => {
process_invite_email(event, email_service, base_url, token_cipher, &settings).await
}
OutboxEventType::EmailInstantLink => {
process_instant_link_email(event, email_service, base_url, token_cipher, &settings)
.await
}
OutboxEventType::EmailSecurityAlert => {
process_security_alert_email(event, email_service, &settings).await
}
OutboxEventType::EmailAccountDeletion => {
process_account_deletion_email(event, email_service, token_cipher, &settings).await
}
_ => Err(AppError::Internal(anyhow::anyhow!(
"Unknown email event type: {}",
event.event_type.as_str()
))),
}
}
async fn process_verification_email(
event: &OutboxEvent,
email_service: &dyn EmailService,
base_url: &str,
token_cipher: &TokenCipher,
settings: &Option<Arc<SettingsService>>,
) -> Result<(), AppError> {
let to = event.payload["to"]
.as_str()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Missing 'to' field")))?;
let verification_url = if let Some(token_enc) = event.payload["token_enc"].as_str() {
let token = token_cipher.decrypt(token_enc)?;
format!("{}/verify-email?token={}", base_url, token)
} else if let Some(url) = event.payload["verification_url"].as_str() {
url.to_string()
} else {
return Err(AppError::Internal(anyhow::anyhow!(
"Missing 'token_enc' field"
)));
};
let data = VerificationEmailData {
user_name: event.payload["user_name"].as_str().map(String::from),
verification_url,
expires_in_hours: event.payload["expires_in_hours"].as_u64().unwrap_or(24) as u32,
};
let subject = get_custom_subject(settings, "email_subject_verification").await;
email_service
.send_verification(to, data, subject.as_deref())
.await
}
async fn process_password_reset_email(
event: &OutboxEvent,
email_service: &dyn EmailService,
base_url: &str,
token_cipher: &TokenCipher,
settings: &Option<Arc<SettingsService>>,
) -> Result<(), AppError> {
let to = event.payload["to"]
.as_str()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Missing 'to' field")))?;
let reset_url = if let Some(token_enc) = event.payload["token_enc"].as_str() {
let token = token_cipher.decrypt(token_enc)?;
format!("{}/reset-password?token={}", base_url, token)
} else if let Some(url) = event.payload["reset_url"].as_str() {
url.to_string()
} else {
return Err(AppError::Internal(anyhow::anyhow!(
"Missing 'token_enc' field"
)));
};
let instant_link_url = event.payload["instant_link_token_enc"]
.as_str()
.map(|enc| token_cipher.decrypt(enc))
.transpose()?
.map(|token| format!("{}/instant-link/verify?token={}", base_url, token));
let has_password = event.payload["has_password"].as_bool().unwrap_or(true);
let data = PasswordResetEmailData {
user_name: event.payload["user_name"].as_str().map(String::from),
reset_url,
expires_in_minutes: event.payload["expires_in_minutes"].as_u64().unwrap_or(60) as u32,
instant_link_url,
has_password,
};
let subject = get_custom_subject(settings, "email_subject_password_reset").await;
email_service
.send_password_reset(to, data, subject.as_deref())
.await
}
async fn process_invite_email(
event: &OutboxEvent,
email_service: &dyn EmailService,
base_url: &str,
token_cipher: &TokenCipher,
settings: &Option<Arc<SettingsService>>,
) -> Result<(), AppError> {
let to = event.payload["to"]
.as_str()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Missing 'to' field")))?;
let accept_url = if let Some(token_enc) = event.payload["token_enc"].as_str() {
let token = token_cipher.decrypt(token_enc)?;
format!("{}/accept-invite?token={}", base_url, token)
} else if let Some(url) = event.payload["accept_url"].as_str() {
url.to_string()
} else {
return Err(AppError::Internal(anyhow::anyhow!(
"Missing 'token_enc' field"
)));
};
let data = InviteEmailData {
org_name: event.payload["org_name"]
.as_str()
.unwrap_or("Organization")
.to_string(),
inviter_name: event.payload["inviter_name"].as_str().map(String::from),
role: event.payload["role"]
.as_str()
.unwrap_or("member")
.to_string(),
accept_url,
expires_in_days: event.payload["expires_in_days"].as_u64().unwrap_or(7) as u32,
};
let subject = get_custom_subject(settings, "email_subject_invite").await;
email_service
.send_invite(to, data, subject.as_deref())
.await
}
async fn process_instant_link_email(
event: &OutboxEvent,
email_service: &dyn EmailService,
base_url: &str,
token_cipher: &TokenCipher,
settings: &Option<Arc<SettingsService>>,
) -> Result<(), AppError> {
let to = event.payload["to"]
.as_str()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Missing 'to' field")))?;
let instant_link_url = if let Some(token_enc) = event.payload["token_enc"].as_str() {
let token = token_cipher.decrypt(token_enc)?;
format!("{}/instant-link/verify?token={}", base_url, token)
} else if let Some(url) = event.payload["instant_link_url"].as_str() {
url.to_string()
} else {
return Err(AppError::Internal(anyhow::anyhow!(
"Missing 'token_enc' field"
)));
};
let data = InstantLinkEmailData {
user_name: event.payload["user_name"].as_str().map(String::from),
instant_link_url,
expires_in_minutes: event.payload["expires_in_minutes"].as_u64().unwrap_or(15) as u32,
};
let subject = get_custom_subject(settings, "email_subject_instant_link").await;
email_service
.send_instant_link(to, data, subject.as_deref())
.await
}
async fn process_security_alert_email(
event: &OutboxEvent,
email_service: &dyn EmailService,
settings: &Option<Arc<SettingsService>>,
) -> Result<(), AppError> {
let to = event.payload["to"]
.as_str()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Missing 'to' field")))?;
let data = SecurityAlertEmailData {
user_name: event.payload["user_name"].as_str().map(String::from),
login_time: event.payload["login_time"]
.as_str()
.unwrap_or("Unknown")
.to_string(),
ip_address: event.payload["ip_address"].as_str().map(String::from),
location: event.payload["location"].as_str().map(String::from),
device: event.payload["device"].as_str().map(String::from),
browser: event.payload["browser"].as_str().map(String::from),
action_url: event.payload["action_url"].as_str().map(String::from),
};
let subject = get_custom_subject(settings, "email_subject_security_alert").await;
email_service
.send_security_alert(to, data, subject.as_deref())
.await
}
async fn process_account_deletion_email(
event: &OutboxEvent,
email_service: &dyn EmailService,
token_cipher: &TokenCipher,
settings: &Option<Arc<SettingsService>>,
) -> Result<(), AppError> {
let to = event.payload["to"]
.as_str()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Missing 'to' field")))?;
let confirmation_base_url = event.payload["confirmation_base_url"]
.as_str()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Missing 'confirmation_base_url' field")))?;
let token_enc = event.payload["token_enc"]
.as_str()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Missing 'token_enc' field")))?;
let token = token_cipher.decrypt(token_enc)?;
let confirmation_url = format!("{confirmation_base_url}?token={token}");
let data = AccountDeletionEmailData {
user_name: event.payload["user_name"].as_str().map(String::from),
confirmation_url,
expires_in_hours: event.payload["expires_in_hours"].as_u64().unwrap_or(24) as u32,
};
let subject = get_custom_subject(settings, "email_subject_account_deletion").await;
email_service
.send_account_deletion(to, data, subject.as_deref())
.await
}