use anyhow::Result; use lettre::{ message::{header::ContentType, Mailbox, MultiPart, SinglePart}, transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor, }; use std::collections::HashMap; use std::env; // ── Template Engine ─────────────────────────────────────────────────────────── pub struct TemplateEngine; impl TemplateEngine { pub fn new() -> Self { Self } pub fn render(&self, template_name: &str, vars: HashMap<&str, &str>) -> Result { let base = include_str!("../templates/base.html"); let content = self.load_template(template_name)?; // Replace content in base template let mut html = base.replace("{{content}}", content); // Replace all variables for (key, value) in vars { html = html.replace(&format!("{{{{{}}}}}", key), value); } Ok(html) } fn load_template(&self, name: &str) -> Result<&str> { match name { // Auth & Account "welcome" => Ok(include_str!("../templates/welcome.html")), "verify-email" => Ok(include_str!("../templates/verify-email.html")), "password-reset" => Ok(include_str!("../templates/password-reset.html")), "password-changed" => Ok(include_str!("../templates/password-changed.html")), "account-suspended" => Ok(include_str!("../templates/account-suspended.html")), // Onboarding & Verification "onboarding-submitted" => Ok(include_str!("../templates/onboarding-submitted.html")), "profile-verified" => Ok(include_str!("../templates/profile-verified.html")), "profile-rejected" => Ok(include_str!("../templates/profile-rejected.html")), "documents-requested" => Ok(include_str!("../templates/documents-requested.html")), // Jobs "job-pending" => Ok(include_str!("../templates/job-pending.html")), "job-approved" => Ok(include_str!("../templates/job-approved.html")), "job-rejected" => Ok(include_str!("../templates/job-rejected.html")), "job-expired" => Ok(include_str!("../templates/job-expired.html")), // Applications "application-received" => Ok(include_str!("../templates/application-received.html")), "application-status" => Ok(include_str!("../templates/application-status.html")), // Requirements "requirement-pending" => Ok(include_str!("../templates/requirement-pending.html")), "requirement-approved" => Ok(include_str!("../templates/requirement-approved.html")), "requirement-expired" => Ok(include_str!("../templates/requirement-expired.html")), // Leads "new-lead-request" => Ok(include_str!("../templates/new-lead-request.html")), "lead-request-sent" => Ok(include_str!("../templates/lead-request-sent.html")), "lead-request-accepted" => Ok(include_str!("../templates/lead-request-accepted.html")), "lead-request-rejected" => Ok(include_str!("../templates/lead-request-rejected.html")), "lead-expired" => Ok(include_str!("../templates/lead-expired.html")), // Payments "payment-success" => Ok(include_str!("../templates/payment-success.html")), "invoice-generated" => Ok(include_str!("../templates/invoice-generated.html")), "manual-credit" => Ok(include_str!("../templates/manual-credit.html")), "credit-usage" => Ok(include_str!("../templates/credit-usage.html")), "low-credit-balance" => Ok(include_str!("../templates/low-credit-balance.html")), // Security "new-device-login" => Ok(include_str!("../templates/new-device-login.html")), // Support "support-ticket-created" => Ok(include_str!("../templates/support-ticket-created.html")), "support-ticket-replied" => Ok(include_str!("../templates/support-ticket-replied.html")), "support-ticket-resolved" => Ok(include_str!("../templates/support-ticket-resolved.html")), // Marketplace "new-matched-lead" => Ok(include_str!("../templates/new-matched-lead.html")), // Policy "policy-update" => Ok(include_str!("../templates/policy-update.html")), _ => Err(anyhow::anyhow!("Template not found: {}", name)), } } } impl Default for TemplateEngine { fn default() -> Self { Self::new() } } // ── Mailer ──────────────────────────────────────────────────────────────────── pub struct Mailer { transport: Option>, from_email: String, from_name: String, template_engine: TemplateEngine, } impl Mailer { pub fn new() -> Self { let smtp_host = env::var("SMTP_HOST").ok(); let smtp_user = env::var("SMTP_USER").ok(); let smtp_pass = env::var("SMTP_PASS").ok(); let smtp_port: u16 = env::var("SMTP_PORT") .ok() .and_then(|p| p.parse().ok()) .unwrap_or(587); let from_email = env::var("SMTP_FROM_EMAIL") .unwrap_or_else(|_| "noreply@nxtgauge.com".to_string()); let from_name = env::var("SMTP_FROM_NAME") .unwrap_or_else(|_| "NXTGAUGE".to_string()); let transport = match (smtp_host, smtp_user, smtp_pass) { (Some(host), Some(user), Some(pass)) => { let creds = Credentials::new(user, pass); match AsyncSmtpTransport::::starttls_relay(&host) { Ok(builder) => { let t = builder.port(smtp_port).credentials(creds).build(); tracing::info!("SMTP transport configured (host={} port={})", host, smtp_port); Some(t) } Err(e) => { tracing::warn!("SMTP transport init failed: {} — emails disabled", e); None } } } _ => { tracing::warn!("SMTP not configured — emails disabled"); None } }; Self { transport, from_email, from_name, template_engine: TemplateEngine::new(), } } async fn send_html(&self, to: &str, subject: &str, html_body: String) -> Result<()> { let Some(transport) = &self.transport else { tracing::debug!("SMTP disabled — skipping email to {}", to); return Ok(()); }; let from: Mailbox = format!("{} <{}>", self.from_name, self.from_email).parse()?; let to: Mailbox = to.parse()?; let email = Message::builder() .from(from) .to(to) .subject(subject) .header(ContentType::TEXT_HTML) .body(html_body)?; transport.send(email).await?; Ok(()) } // ── Auth & Account ──────────────────────────────────────────────────────── pub async fn send_verification_email(&self, to: &str, name: &str, otp: &str) -> Result<()> { let vars = HashMap::from([ ("first_name", name), ("otp_code", otp), ]); let html = self.template_engine.render("verify-email", vars)?; self.send_html(to, "Verify Your Email Address", html).await } pub async fn send_password_reset_email(&self, to: &str, name: &str, token: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let reset_url = format!("{}/reset-password?token={}", frontend_url, token); let vars = HashMap::from([ ("first_name", name), ("reset_url", &reset_url), ]); let html = self.template_engine.render("password-reset", vars)?; self.send_html(to, "Reset Your Password", html).await } pub async fn send_welcome_email(&self, to: &str, name: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let dashboard_url = format!("{}/dashboard", frontend_url); let vars = HashMap::from([ ("first_name", name), ("dashboard_url", &dashboard_url), ]); let html = self.template_engine.render("welcome", vars)?; self.send_html(to, "Welcome to Nxtgauge!", html).await } // ── Profile Verification ─────────────────────────────────────────────────── pub async fn send_profile_verified_email(&self, to: &str, name: &str, role_name: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let now = chrono::Local::now().format("%B %d, %Y").to_string(); let vars = HashMap::from([ ("first_name", name), ("role_name", role_name), ("verified_at", &now), ("dashboard_url", &frontend_url), ]); let html = self.template_engine.render("profile-verified", vars)?; self.send_html(to, "Your Profile is Verified!", html).await } pub async fn send_profile_rejected_email(&self, to: &str, name: &str, role_name: &str, reason: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let profile_url = format!("{}/dashboard/profile", frontend_url); let vars = HashMap::from([ ("first_name", name), ("role_name", role_name), ("rejection_reason", reason), ("profile_url", &profile_url), ]); let html = self.template_engine.render("profile-rejected", vars)?; self.send_html(to, "Profile Verification Update", html).await } // ── Jobs ─────────────────────────────────────────────────────────────────── pub async fn send_job_pending_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> { let now = chrono::Local::now().format("%B %d, %Y").to_string(); let vars = HashMap::from([ ("first_name", company_name), ("job_title", job_title), ("posted_at", &now), ]); let html = self.template_engine.render("job-pending", vars)?; self.send_html(to, "Job Posted Successfully", html).await } pub async fn send_job_approved_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let now = chrono::Local::now().format("%B %d, %Y").to_string(); let expires = (chrono::Local::now() + chrono::Duration::days(30)).format("%B %d, %Y").to_string(); let job_url = format!("{}/dashboard/jobs", frontend_url); let vars = HashMap::from([ ("first_name", company_name), ("job_title", job_title), ("approved_at", &now), ("expires_at", &expires), ("job_url", &job_url), ]); let html = self.template_engine.render("job-approved", vars)?; self.send_html(to, "Your Job is Now Live!", html).await } pub async fn send_job_rejected_email(&self, to: &str, company_name: &str, job_title: &str, reason: &str) -> Result<()> { let vars = HashMap::from([ ("first_name", company_name), ("job_title", job_title), ("rejection_reason", reason), ]); let html = self.template_engine.render("job-rejected", vars)?; self.send_html(to, "Job Posting Needs Updates", html).await } pub async fn send_application_received_email(&self, to: &str, company_name: &str, job_title: &str, applicant_name: &str, total_apps: i64) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let apps_url = format!("{}/dashboard/jobs", frontend_url); let total_str = total_apps.to_string(); let vars = HashMap::from([ ("first_name", company_name), ("job_title", job_title), ("applicant_name", applicant_name), ("total_applications", &total_str), ("applications_url", &apps_url), ]); let html = self.template_engine.render("application-received", vars)?; self.send_html(to, &format!("New Application: {}", job_title), html).await } // ── Leads ────────────────────────────────────────────────────────────────── pub async fn send_lead_request_sent_email(&self, to: &str, name: &str, requirement_title: &str, location: &str, reserved: i32) -> Result<()> { let reserved_str = reserved.to_string(); let vars = HashMap::from([ ("first_name", name), ("requirement_title", requirement_title), ("location", location), ("tracecoins_reserved", &reserved_str), ]); let html = self.template_engine.render("lead-request-sent", vars)?; self.send_html(to, "Lead Request Sent", html).await } pub async fn send_lead_request_accepted_email(&self, to: &str, professional_name: &str, customer_name: &str, requirement_title: &str, deducted: i32) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let now = chrono::Local::now().format("%B %d, %Y").to_string(); let lead_url = format!("{}/dashboard/leads/accepted", frontend_url); let deducted_str = deducted.to_string(); let vars = HashMap::from([ ("first_name", professional_name), ("customer_name", customer_name), ("requirement_title", requirement_title), ("tracecoins_deducted", &deducted_str), ("accepted_at", &now), ("lead_url", &lead_url), ]); let html = self.template_engine.render("lead-request-accepted", vars)?; self.send_html(to, "Lead Request Accepted!", html).await } pub async fn send_new_lead_request_email(&self, to: &str, customer_name: &str, requirement_title: &str, professional_name: &str, profession: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let now = chrono::Local::now().format("%B %d, %Y at %I:%M %p").to_string(); let req_url = format!("{}/dashboard/requirements", frontend_url); let vars = HashMap::from([ ("first_name", customer_name), ("requirement_title", requirement_title), ("professional_name", professional_name), ("profession_type", profession), ("requested_at", &now), ("requirement_url", &req_url), ]); let html = self.template_engine.render("new-lead-request", vars)?; self.send_html(to, &format!("New Request: {}", requirement_title), html).await } // ── Payments ─────────────────────────────────────────────────────────────── pub async fn send_payment_success_email(&self, to: &str, name: &str, package_name: &str, amount: i32, tracecoins: i32, transaction_id: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let invoice_url = format!("{}/dashboard/wallet/invoices", frontend_url); let wallet_url = format!("{}/dashboard/wallet", frontend_url); let amount_str = format!("{:.2}", amount as f32 / 100.0); let tc_str = tracecoins.to_string(); let now = chrono::Local::now().format("%B %d, %Y").to_string(); let vars = HashMap::from([ ("first_name", name), ("package_name", package_name), ("amount_paid", &amount_str), ("tracecoins_amount", &tc_str), ("transaction_id", transaction_id), ("payment_date", &now), ("invoice_url", &invoice_url), ("wallet_url", &wallet_url), ]); let html = self.template_engine.render("payment-success", vars)?; self.send_html(to, "Payment Successful!", html).await } // ── Account ───────────────────────────────────────────────────────────────── pub async fn send_account_suspended_email(&self, to: &str, name: &str, reason: &str) -> Result<()> { let vars = HashMap::from([ ("first_name", name), ("suspension_reason", reason), ]); let html = self.template_engine.render("account-suspended", vars)?; self.send_html(to, "Account Suspended", html).await } // ── Cron / Expiry ─────────────────────────────────────────────────────────── pub async fn send_lead_expired_email(&self, to: &str, name: &str, tracecoins: i32) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let tc_str = tracecoins.to_string(); let marketplace_url = format!("{}/dashboard/marketplace", frontend_url); let vars = HashMap::from([ ("first_name", name), ("tracecoins_returned", &tc_str), ("marketplace_url", &marketplace_url), ]); let html = self.template_engine.render("lead-expired", vars)?; self.send_html(to, "Lead Request Expired", html).await } pub async fn send_requirement_expired_email(&self, to: &str, name: &str, title: &str) -> Result<()> { let vars = HashMap::from([ ("first_name", name), ("requirement_title", title), ]); let html = self.template_engine.render("requirement-expired", vars)?; self.send_html(to, "Requirement Expired", html).await } pub async fn send_job_expired_email(&self, to: &str, company_name: &str, title: &str) -> Result<()> { let vars = HashMap::from([ ("first_name", company_name), ("job_title", title), ]); let html = self.template_engine.render("job-expired", vars)?; self.send_html(to, "Job Posting Expired", html).await } // ── Missing Methods Added ─────────────────────────────────────────────────── pub async fn send_password_changed_email(&self, to: &str, name: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let now = chrono::Local::now().format("%B %d, %Y at %I:%M %p").to_string(); let security_url = format!("{}/dashboard/settings", frontend_url); let vars = HashMap::from([ ("first_name", name), ("email", to), ("changed_at", &now), ("security_url", &security_url), ]); let html = self.template_engine.render("password-changed", vars)?; self.send_html(to, "Password Changed Successfully", html).await } pub async fn send_account_deleted_email(&self, to: &str, name: &str) -> Result<()> { let vars = HashMap::from([ ("first_name", name), ]); // Use account-suspended template as base or create a simple message self.send_html( to, "Account Deleted", format!("

Account Deleted

Hi {},

Your account has been deleted.

", name) ).await } pub async fn send_approval_approved_email(&self, to: &str, name: &str, role: &str) -> Result<()> { self.send_profile_verified_email(to, name, role).await } pub async fn send_approval_rejected_email(&self, to: &str, name: &str, role: &str, reason: &str) -> Result<()> { self.send_profile_rejected_email(to, name, role, reason).await } pub async fn send_job_submitted_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> { self.send_job_pending_email(to, company_name, job_title).await } pub async fn send_new_application_email(&self, to: &str, company_name: &str, job_title: &str, applicant_name: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let apps_url = format!("{}/dashboard/jobs", frontend_url); let vars = HashMap::from([ ("first_name", company_name), ("job_title", job_title), ("applicant_name", applicant_name), ("total_applications", "1"), ("applications_url", &apps_url), ]); let html = self.template_engine.render("application-received", vars)?; self.send_html(to, &format!("New Application: {}", job_title), html).await } pub async fn send_requirement_submitted_email(&self, to: &str, name: &str, title: &str) -> Result<()> { let now = chrono::Local::now().format("%B %d, %Y").to_string(); let vars = HashMap::from([ ("first_name", name), ("requirement_title", title), ("profession_type", "Service"), ("posted_at", &now), ]); let html = self.template_engine.render("requirement-pending", vars)?; self.send_html(to, "Requirement Submitted Successfully", html).await } pub async fn send_lead_accepted_customer_email(&self, to: &str, customer_name: &str, professional_name: &str, professional_email: &str, professional_phone: &str) -> Result<()> { let vars = HashMap::from([ ("first_name", customer_name), ("professional_name", professional_name), ("professional_email", professional_email), ("professional_phone", professional_phone), ]); // Use a simple template or new-lead-request as base self.send_html( to, "Professional Contact Details", format!("

Professional Contact

Hi {},

You accepted {}'s request.

Email: {}

Phone: {}

", customer_name, professional_name, professional_email, professional_phone) ).await } pub async fn send_manual_credit_email(&self, to: &str, name: &str, amount: i32, reason: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let wallet_url = format!("{}/dashboard/wallet", frontend_url); let amount_str = amount.to_string(); let now = chrono::Local::now().format("%B %d, %Y").to_string(); let vars = HashMap::from([ ("first_name", name), ("tracecoins_amount", &amount_str), ("credited_at", &now), ("reason", reason), ("wallet_url", &wallet_url), ]); let html = self.template_engine.render("manual-credit", vars)?; self.send_html(to, "Tracecoins Credited to Your Account", html).await } // ── Support Tickets ───────────────────────────────────────────────────────── pub async fn send_support_ticket_created_email(&self, to: &str, name: &str, ticket_id: &str, subject: &str, category: &str, priority: &str, response_time: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let ticket_url = format!("{}/dashboard/support/{}", frontend_url, ticket_id); let vars = HashMap::from([ ("first_name", name), ("ticket_id", ticket_id), ("subject", subject), ("category", category), ("priority", priority), ("response_time", response_time), ("ticket_url", &ticket_url), ]); let html = self.template_engine.render("support-ticket-created", vars)?; self.send_html(to, "Support Ticket Received", html).await } pub async fn send_support_ticket_replied_email(&self, to: &str, name: &str, subject: &str, message: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let ticket_url = format!("{}/dashboard/support", frontend_url); let vars = HashMap::from([ ("first_name", name), ("subject", subject), ("latest_message", message), ("support_agent_name", "Nxtgauge Support Team"), ("ticket_url", &ticket_url), ]); let html = self.template_engine.render("support-ticket-replied", vars)?; self.send_html(to, "New Response on Your Ticket", html).await } pub async fn send_support_ticket_resolved_email(&self, to: &str, name: &str, subject: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let now = chrono::Local::now().format("%B %d, %Y").to_string(); let vars = HashMap::from([ ("first_name", name), ("subject", subject), ("resolved_at", &now), ("support_agent_name", "Nxtgauge Support Team"), ]); let html = self.template_engine.render("support-ticket-resolved", vars)?; self.send_html(to, "Ticket Resolved", html).await } // ── Documents Requested ───────────────────────────────────────────────────── pub async fn send_documents_requested_email(&self, to: &str, name: &str, role_name: &str, document_request: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let profile_url = format!("{}/dashboard/profile", frontend_url); let vars = HashMap::from([ ("first_name", name), ("role_name", role_name), ("document_request", document_request), ("profile_url", &profile_url), ]); let html = self.template_engine.render("documents-requested", vars)?; self.send_html(to, "Additional Documents Required", html).await } // ── Application Status ────────────────────────────────────────────────────── pub async fn send_application_status_email(&self, to: &str, name: &str, job_title: &str, status: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string()); let apps_url = format!("{}/dashboard/applications", frontend_url); let (status_class, status_color, next_steps) = match status { "SHORTLISTED" => ("status-pending", "#d97706", "The employer has shortlisted you. They may contact you soon for an interview."), "INTERVIEW" => ("status-pending", "#d97706", "Congratulations! You've been selected for an interview. Check your email for scheduling details."), "OFFERED" => ("status-approved", "#059669", "Great news! You've received a job offer. Please review the offer details in your dashboard."), "HIRED" => ("status-approved", "#059669", "Congratulations on your new job! The employer will contact you with next steps."), "REJECTED" => ("status-rejected", "#dc2626", "Unfortunately, you were not selected for this position. Don't give up - apply to other jobs!"), _ => ("status-pending", "#6b7280", "Your application status has been updated."), }; let now = chrono::Local::now().format("%B %d, %Y").to_string(); let vars = HashMap::from([ ("first_name", name), ("job_title", job_title), ("company_name", "The employer"), ("status", status), ("status_class", status_class), ("status_color", status_color), ("next_steps", next_steps), ("note", ""), ("updated_at", &now), ("applications_url", &apps_url), ]); let html = self.template_engine.render("application-status", vars)?; self.send_html(to, &format!("Application Update: {}", job_title), html).await } // ── Test Email ────────────────────────────────────────────────────────────── pub async fn send_test_email(&self, to: &str) -> Result<()> { let html = r##" "##; self.send_html(to, "SMTP Test - Nxtgauge Email System", html.to_string()).await } } impl Default for Mailer { fn default() -> Self { Self::new() } }