nxtgauge-backend-rust/crates/email/src/lib.rs

366 lines
16 KiB
Rust
Raw Normal View History

use anyhow::Result;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport,
Message, Tokio1Executor,
};
use std::env;
// ── Mailer ────────────────────────────────────────────────────────────────────
pub struct Mailer {
transport: Option<AsyncSmtpTransport<Tokio1Executor>>,
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::<Tokio1Executor>::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
}
pub async fn send_account_deleted_email(&self, to: &str, name: &str) -> Result<()> {
self.send(
to,
"Your NXTGAUGE account has been deleted",
format!(
"Hello {},\n\nYour account has been deleted as requested. If this was not you, please contact support immediately.\n\nRegards,\nThe NXTGAUGE Team",
name
),
).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 12 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()
}
}