refactor: unify email sending to use EmailService + Vault, remove Gmail hardcoded defaults
Some checks failed
BotServer CI/CD / build (push) Failing after 17s
Some checks failed
BotServer CI/CD / build (push) Failing after 17s
- Replace gmail defaults with Vault-backed get_email_config_for_bot_sync - send_campaign_email now delegates to EmailService::send_email - Remove hardcoded smtp.gmail.com, imap.gmail.com, noreply@generalbots.com - All SMTP config flows through Vault: bot → default bot → system - Remove unused lettre imports from marketing/email.rs
This commit is contained in:
parent
b131c7e311
commit
1dc11c9b4e
3 changed files with 31 additions and 89 deletions
|
|
@ -298,12 +298,12 @@ impl AppConfig {
|
||||||
secret_key: drive_secret,
|
secret_key: drive_secret,
|
||||||
};
|
};
|
||||||
let email = EmailConfig {
|
let email = EmailConfig {
|
||||||
server: get_str("EMAIL_IMAP_SERVER", "imap.gmail.com"),
|
server: get_str("EMAIL_IMAP_SERVER", ""),
|
||||||
port: get_u16("EMAIL_IMAP_PORT", 993),
|
port: get_u16("EMAIL_IMAP_PORT", 993),
|
||||||
username: get_str("EMAIL_USERNAME", ""),
|
username: get_str("EMAIL_USERNAME", ""),
|
||||||
password: get_str("EMAIL_PASSWORD", ""),
|
password: get_str("EMAIL_PASSWORD", ""),
|
||||||
from: get_str("EMAIL_FROM", ""),
|
from: get_str("EMAIL_FROM", ""),
|
||||||
smtp_server: get_str("EMAIL_SMTP_SERVER", "smtp.gmail.com"),
|
smtp_server: get_str("EMAIL_SMTP_SERVER", ""),
|
||||||
smtp_port: get_u16("EMAIL_SMTP_PORT", 587),
|
smtp_port: get_u16("EMAIL_SMTP_PORT", 587),
|
||||||
};
|
};
|
||||||
let port = std::env::var("PORT")
|
let port = std::env::var("PORT")
|
||||||
|
|
|
||||||
|
|
@ -403,14 +403,14 @@ impl SecretsManager {
|
||||||
});
|
});
|
||||||
if let Ok(Some(secrets)) = rx.recv() {
|
if let Ok(Some(secrets)) = rx.recv() {
|
||||||
return (
|
return (
|
||||||
secrets.get("smtp_host").cloned().unwrap_or_else(|| "smtp.gmail.com".into()),
|
secrets.get("smtp_host").cloned().unwrap_or_default(),
|
||||||
secrets.get("smtp_port").and_then(|p| p.parse().ok()).unwrap_or(587),
|
secrets.get("smtp_port").and_then(|p| p.parse().ok()).unwrap_or(587),
|
||||||
secrets.get("smtp_user").cloned().unwrap_or_default(),
|
secrets.get("smtp_user").cloned().unwrap_or_default(),
|
||||||
secrets.get("smtp_password").cloned().unwrap_or_default(),
|
secrets.get("smtp_password").cloned().unwrap_or_default(),
|
||||||
secrets.get("smtp_from").cloned().unwrap_or_default(),
|
secrets.get("smtp_from").cloned().unwrap_or_default(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
("smtp.gmail.com".to_string(), 587, String::new(), String::new(), String::new())
|
(String::new(), 587, String::new(), String::new(), String::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_llm_config(&self) -> (String, String, Option<String>, Option<String>, String) {
|
pub fn get_llm_config(&self) -> (String, String, Option<String>, Option<String>, String) {
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,10 @@ use axum::{
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use lettre::{message::Mailbox, Address, Message, SmtpTransport, Transport};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::core::config::ConfigManager;
|
|
||||||
use crate::core::shared::schema::{
|
|
||||||
email_tracking, marketing_campaigns, marketing_recipients,
|
|
||||||
};
|
|
||||||
use crate::core::shared::state::AppState;
|
|
||||||
use crate::marketing::campaigns::CrmCampaign;
|
|
||||||
|
|
||||||
const SMTP_SERVER_KEY: &str = "email-server";
|
|
||||||
const SMTP_PORT_KEY: &str = "email-port";
|
|
||||||
const FROM_EMAIL_KEY: &str = "email-from";
|
|
||||||
const DEFAULT_SMTP_SERVER: &str = "smtp.gmail.com";
|
|
||||||
const DEFAULT_SMTP_PORT: u16 = 587;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct EmailCampaignPayload {
|
pub struct EmailCampaignPayload {
|
||||||
pub to: String,
|
pub to: String,
|
||||||
|
|
@ -68,31 +54,16 @@ pub struct CampaignMetrics {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_smtp_config(state: &AppState, bot_id: Uuid) -> Result<(String, u16, String, String, String), String> {
|
fn get_smtp_config(state: &AppState, bot_id: Uuid) -> Result<(String, u16, String, String, String), String> {
|
||||||
let config = ConfigManager::new(state.conn.clone());
|
let secrets = crate::core::secrets::SecretsManager::from_env()
|
||||||
|
.map_err(|e| format!("Vault not available: {}", e))?;
|
||||||
|
let (smtp_host, smtp_port, smtp_user, smtp_pass, smtp_from) =
|
||||||
|
secrets.get_email_config_for_bot_sync(&bot_id);
|
||||||
|
|
||||||
let smtp_server = config
|
if smtp_from.is_empty() {
|
||||||
.get_config(&bot_id, SMTP_SERVER_KEY, Some(DEFAULT_SMTP_SERVER))
|
return Err("SMTP not configured: set email credentials in Vault".into());
|
||||||
.unwrap_or_else(|_| DEFAULT_SMTP_SERVER.to_string());
|
}
|
||||||
|
|
||||||
let smtp_port = config
|
Ok((smtp_host, smtp_port, smtp_from, smtp_user, smtp_pass))
|
||||||
.get_config(&bot_id, SMTP_PORT_KEY, Some("587"))
|
|
||||||
.unwrap_or_else(|_| "587".to_string())
|
|
||||||
.parse::<u16>()
|
|
||||||
.unwrap_or(DEFAULT_SMTP_PORT);
|
|
||||||
|
|
||||||
let from_email = config
|
|
||||||
.get_config(&bot_id, FROM_EMAIL_KEY, Some("noreply@generalbots.com"))
|
|
||||||
.unwrap_or_else(|_| "noreply@generalbots.com".to_string());
|
|
||||||
|
|
||||||
let smtp_username = config
|
|
||||||
.get_config(&bot_id, "email-username", None)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let smtp_password = config
|
|
||||||
.get_config(&bot_id, "email-password", None)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
Ok((smtp_server, smtp_port, from_email, smtp_username, smtp_password))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inject_tracking_pixel(html: &str, token: Uuid, base_url: &str) -> String {
|
fn inject_tracking_pixel(html: &str, token: Uuid, base_url: &str) -> String {
|
||||||
|
|
@ -126,9 +97,6 @@ pub async fn send_campaign_email(
|
||||||
bot_id: Uuid,
|
bot_id: Uuid,
|
||||||
payload: EmailCampaignPayload,
|
payload: EmailCampaignPayload,
|
||||||
) -> Result<EmailSendResult, String> {
|
) -> Result<EmailSendResult, String> {
|
||||||
let (smtp_server, smtp_port, from_email, username, password) =
|
|
||||||
get_smtp_config(state, bot_id)?;
|
|
||||||
|
|
||||||
let open_token = Uuid::new_v4();
|
let open_token = Uuid::new_v4();
|
||||||
let tracking_id = Uuid::new_v4();
|
let tracking_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
|
@ -171,51 +139,11 @@ pub async fn send_campaign_email(
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
.map_err(|e| format!("Failed to create tracking record: {}", e))?;
|
.map_err(|e| format!("Failed to create tracking record: {}", e))?;
|
||||||
|
|
||||||
let from = from_email
|
let body = body_html.unwrap_or_else(|| payload.body_text.unwrap_or_default());
|
||||||
.parse::<Address>()
|
|
||||||
.map_err(|e| format!("Invalid from address: {}", e))?;
|
|
||||||
let to = payload
|
|
||||||
.to
|
|
||||||
.parse::<Address>()
|
|
||||||
.map_err(|e| format!("Invalid to address: {}", e))?;
|
|
||||||
|
|
||||||
let builder = Message::builder()
|
let email_service = EmailService::new(state.clone());
|
||||||
.from(Mailbox::new(Some("Campaign".to_string()), from))
|
match email_service.send_email(&payload.to, &payload.subject, &body, bot_id, None) {
|
||||||
.to(Mailbox::new(None, to))
|
Ok(message_id) => {
|
||||||
.subject(&payload.subject);
|
|
||||||
|
|
||||||
let mail = if let Some(html) = &body_html {
|
|
||||||
builder
|
|
||||||
.header(lettre::message::header::ContentType::TEXT_HTML)
|
|
||||||
.body(html.clone())
|
|
||||||
.map_err(|e| format!("Failed to build HTML email: {}", e))?
|
|
||||||
} else if let Some(text) = &payload.body_text {
|
|
||||||
builder
|
|
||||||
.header(lettre::message::header::ContentType::TEXT_PLAIN)
|
|
||||||
.body(text.clone())
|
|
||||||
.map_err(|e| format!("Failed to build text email: {}", e))?
|
|
||||||
} else {
|
|
||||||
return Err("No email body provided".to_string());
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut mailer = SmtpTransport::relay(&smtp_server)
|
|
||||||
.map_err(|e| format!("SMTP relay error: {}", e))?
|
|
||||||
.port(smtp_port);
|
|
||||||
|
|
||||||
if !username.is_empty() && !password.is_empty() {
|
|
||||||
mailer = mailer
|
|
||||||
.credentials(lettre::transport::smtp::authentication::Credentials::new(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
))
|
|
||||||
.authentication(vec![lettre::transport::smtp::authentication::Mechanism::Login]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mailer = mailer.build();
|
|
||||||
|
|
||||||
match mailer.send(&mail) {
|
|
||||||
Ok(_) => {
|
|
||||||
let message_id = format!("<{}@campaign>", tracking_id);
|
|
||||||
diesel::update(email_tracking::table.filter(email_tracking::id.eq(tracking_id)))
|
diesel::update(email_tracking::table.filter(email_tracking::id.eq(tracking_id)))
|
||||||
.set(email_tracking::message_id.eq(Some(message_id.clone())))
|
.set(email_tracking::message_id.eq(Some(message_id.clone())))
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
|
|
@ -244,7 +172,21 @@ pub async fn send_campaign_email(
|
||||||
.set((
|
.set((
|
||||||
marketing_recipients::status.eq("failed"),
|
marketing_recipients::status.eq("failed"),
|
||||||
marketing_recipients::failed_at.eq(Some(Utc::now())),
|
marketing_recipients::failed_at.eq(Some(Utc::now())),
|
||||||
marketing_recipients::error_message.eq(Some(e.to_string())),
|
marketing_recipients::error_message.eq(Some(e.clone())),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EmailSendResult {
|
||||||
|
success: false,
|
||||||
|
message_id: None,
|
||||||
|
tracking_id: Some(tracking_id),
|
||||||
|
error: Some(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
))
|
))
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue