use anyhow::Result; use lettre::{ transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor, }; use std::env; // ── Mailer ──────────────────────────────────────────────────────────────────── pub struct Mailer { transport: Option>, from_email: String, from_name: String, } impl Mailer { /// Build from environment variables. SMTP is optional — if vars are missing emails are /// silently skipped so development works without a real SMTP server. 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={}:{})", host, smtp_port); Some(t) } Err(e) => { tracing::warn!("SMTP transport init failed: {} — emails disabled", e); None } } } _ => { tracing::warn!("SMTP_HOST/SMTP_USER/SMTP_PASS not all set — email disabled"); None } }; Self { transport, from_email, from_name } } async fn send(&self, to: &str, subject: &str, body: String) -> Result<()> { let Some(transport) = &self.transport else { tracing::debug!("SMTP disabled — skipping email to {} (subject: {})", to, subject); return Ok(()); }; let email = Message::builder() .from(format!("{} <{}>", self.from_name, self.from_email).parse()?) .to(to.parse()?) .subject(subject) .body(body)?; transport.send(email).await?; Ok(()) } // ── Auth ────────────────────────────────────────────────────────────────── pub async fn send_verification_email(&self, to: &str, name: &str, otp: &str) -> Result<()> { self.send( to, "Verify your NXTGAUGE account", format!( "Hello {},\n\nYour verification code is: {}\n\nThis code expires in 15 minutes.\n\nRegards,\nThe NXTGAUGE Team", name, otp ), ).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(|_| "http://localhost:3000".to_string()); self.send( to, "Reset your NXTGAUGE password", format!( "Hello {},\n\nClick to reset your password:\n{}/reset-password?token={}\n\nIf you did not request this, ignore this email.\n\nRegards,\nThe NXTGAUGE Team", name, frontend_url, token ), ).await } pub async fn send_password_changed_email(&self, to: &str, name: &str) -> Result<()> { self.send( to, "Your NXTGAUGE password was changed", format!( "Hello {},\n\nYour password was successfully changed. If you did not do this, contact support immediately.\n\nRegards,\nThe NXTGAUGE Team", name ), ).await } pub async fn send_account_suspended_email(&self, to: &str, name: &str, reason: &str) -> Result<()> { self.send( to, "Your NXTGAUGE account has been suspended", format!( "Hello {},\n\nYour account has been suspended.\n\nReason: {}\n\nIf you believe this is a mistake, contact support.\n\nRegards,\nThe NXTGAUGE Team", name, reason ), ).await } // ── Onboarding & Approvals ──────────────────────────────────────────────── pub async fn send_onboarding_submitted_email(&self, to: &str, name: &str, role: &str) -> Result<()> { self.send( to, "Your NXTGAUGE profile is under review", format!( "Hello {},\n\nThank you for submitting your {} profile on NXTGAUGE. Our team will review it within 1–2 business days.\n\nYou will receive an email once your profile is approved.\n\nRegards,\nThe NXTGAUGE Team", name, role ), ).await } pub async fn send_approval_approved_email(&self, to: &str, name: &str, role: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()); self.send( to, "Your NXTGAUGE profile is approved!", format!( "Hello {},\n\nGreat news! Your {} profile on NXTGAUGE has been approved. You can now access your full dashboard.\n\n{}/dashboard\n\nRegards,\nThe NXTGAUGE Team", name, role, frontend_url ), ).await } pub async fn send_approval_rejected_email(&self, to: &str, name: &str, role: &str, reason: &str) -> Result<()> { self.send( to, "Update required on your NXTGAUGE profile", format!( "Hello {},\n\nUnfortunately, we were unable to approve your {} profile at this time.\n\nReason: {}\n\nPlease update your profile and resubmit. If you have questions, contact support.\n\nRegards,\nThe NXTGAUGE Team", name, role, reason ), ).await } // ── Jobs (Company) ──────────────────────────────────────────────────────── pub async fn send_job_submitted_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> { self.send( to, "Your job posting is under review", format!( "Hello {},\n\nYour job posting \"{}\" has been submitted for review. It will go live once our team approves it (usually within 24 hours).\n\nRegards,\nThe NXTGAUGE Team", company_name, job_title ), ).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(|_| "http://localhost:3000".to_string()); self.send( to, "Your job posting is now live!", format!( "Hello {},\n\nYour job posting \"{}\" has been approved and is now live on NXTGAUGE.\n\n{}/dashboard/jobs\n\nRegards,\nThe NXTGAUGE Team", company_name, job_title, frontend_url ), ).await } pub async fn send_job_rejected_email(&self, to: &str, company_name: &str, job_title: &str, reason: &str) -> Result<()> { self.send( to, "Your job posting needs updates", format!( "Hello {},\n\nYour job posting \"{}\" could not be approved.\n\nReason: {}\n\nPlease update and resubmit from your dashboard.\n\nRegards,\nThe NXTGAUGE Team", company_name, job_title, reason ), ).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(|_| "http://localhost:3000".to_string()); self.send( to, &format!("New application for \"{}\"", job_title), format!( "Hello {},\n\n{} has applied for your job posting \"{}\".\n\nReview the application:\n{}/dashboard/jobs\n\nRegards,\nThe NXTGAUGE Team", company_name, applicant_name, job_title, frontend_url ), ).await } pub async fn send_application_status_email(&self, to: &str, applicant_name: &str, job_title: &str, status: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()); let status_label = match status { "SHORTLISTED" => "shortlisted", "INTERVIEW" => "selected for an interview", "OFFERED" => "offered the position", "HIRED" => "hired", "REJECTED" => "not selected at this time", _ => status, }; self.send( to, &format!("Update on your application for \"{}\"", job_title), format!( "Hello {},\n\nYour application for \"{}\" has been updated: you have been {}.\n\nView details:\n{}/dashboard/applications\n\nRegards,\nThe NXTGAUGE Team", applicant_name, job_title, status_label, frontend_url ), ).await } // ── Requirements (Customer) ─────────────────────────────────────────────── pub async fn send_requirement_submitted_email(&self, to: &str, name: &str, title: &str) -> Result<()> { self.send( to, "Your requirement is under review", format!( "Hello {},\n\nYour requirement \"{}\" has been submitted for review and will go live once approved.\n\nRegards,\nThe NXTGAUGE Team", name, title ), ).await } pub async fn send_requirement_approved_email(&self, to: &str, name: &str, title: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()); self.send( to, "Your requirement is now live!", format!( "Hello {},\n\nYour requirement \"{}\" is now live and professionals can send you requests.\n\n{}/dashboard/requirements\n\nRegards,\nThe NXTGAUGE Team", name, title, frontend_url ), ).await } pub async fn send_lead_request_received_email(&self, to: &str, customer_name: &str, requirement_title: &str, professional_name: &str) -> Result<()> { let frontend_url = env::var("FRONTEND_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()); self.send( to, &format!("New request for \"{}\"", requirement_title), format!( "Hello {},\n\n{} is interested in your requirement \"{}\" and has sent a request to connect.\n\nReview and accept/reject:\n{}/dashboard/requirements\n\nRegards,\nThe NXTGAUGE Team", customer_name, professional_name, requirement_title, frontend_url ), ).await } pub async fn send_lead_accepted_professional_email(&self, to: &str, professional_name: &str, customer_name: &str, customer_email: &str, customer_phone: &str) -> Result<()> { self.send( to, "Your lead request was accepted!", format!( "Hello {},\n\n{} has accepted your request. Here are their contact details:\n\nEmail: {}\nPhone: {}\n\nPlease reach out to them directly.\n\nRegards,\nThe NXTGAUGE Team", professional_name, customer_name, customer_email, customer_phone ), ).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<()> { self.send( to, "You accepted a professional request", format!( "Hello {},\n\nYou accepted {}'s request. Here are their contact details:\n\nEmail: {}\nPhone: {}\n\nRegards,\nThe NXTGAUGE Team", customer_name, professional_name, professional_email, professional_phone ), ).await } pub async fn send_lead_rejected_email(&self, to: &str, professional_name: &str, requirement_title: &str) -> Result<()> { self.send( to, "Your lead request was not accepted", format!( "Hello {},\n\nYour request for the requirement \"{}\" was not accepted this time. Your Tracecoins have been returned to your wallet.\n\nKeep exploring the marketplace for more opportunities!\n\n{}/dashboard/marketplace\n\nRegards,\nThe NXTGAUGE Team", professional_name, requirement_title, env::var("FRONTEND_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()) ), ).await } // ── Tracecoins ──────────────────────────────────────────────────────────── pub async fn send_manual_credit_email(&self, to: &str, name: &str, amount: i32, reason: &str) -> Result<()> { self.send( to, "Tracecoins credited to your account", format!( "Hello {},\n\n{} Tracecoins have been credited to your NXTGAUGE wallet.\n\nReason: {}\n\nRegards,\nThe NXTGAUGE Team", name, amount, reason ), ).await } // ── Expiry (Cron) ───────────────────────────────────────────────────────── pub async fn send_lead_expired_email(&self, to: &str, name: &str, tracecoins_returned: i32) -> Result<()> { self.send( to, "Your lead request has expired", format!( "Hello {},\n\nYour lead request has expired because it wasn't accepted within 24 hours.\n\nWe have refunded your {} reserved Tracecoins back to your wallet.\n\nRegards,\nThe NXTGAUGE Team", name, tracecoins_returned ), ).await } pub async fn send_requirement_expired_email(&self, to: &str, name: &str, title: &str) -> Result<()> { self.send( to, "Your requirement has expired", format!( "Hello {},\n\nYour requirement \"{}\" has expired and is no longer visible to professionals.\n\nRegards,\nThe NXTGAUGE Team", name, title ), ).await } pub async fn send_job_expired_email(&self, to: &str, company_name: &str, title: &str) -> Result<()> { self.send( to, "Your job posting has expired", format!( "Hello {},\n\nYour job posting \"{}\" has expired and is no longer accepting applications.\n\nRegards,\nThe NXTGAUGE Team", company_name, title ), ).await } } impl Default for Mailer { fn default() -> Self { Self::new() } }