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

365 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}