Update Woodpecker CI/CD configs and backend: add .woodpecker/ directory, update base/dockerhub/yml configs, Cargo.lock, email handler and crate
This commit is contained in:
parent
614357cfa7
commit
9313f1288c
9 changed files with 402 additions and 109 deletions
|
|
@ -10,8 +10,9 @@ steps:
|
|||
- name: build-base-image
|
||||
image: woodpeckerci/plugin-docker-buildx:5.0.0
|
||||
settings:
|
||||
registry: ghcr.io
|
||||
repo: ghcr.io/traceworks2023/nxtgauge-rust-base
|
||||
registry:
|
||||
from_secret: REGISTRY_HOSTPORT
|
||||
repo: nxtgauge-rust-base
|
||||
context: .
|
||||
dockerfile: Dockerfile.base
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -87,8 +87,9 @@ steps:
|
|||
- name: build-docker
|
||||
image: woodpeckerci/plugin-docker-buildx:5.0.0
|
||||
settings:
|
||||
registry: docker.io
|
||||
repo: your-dockerhub-username/nxtgauge-rust-${SERVICE}
|
||||
registry:
|
||||
from_secret: REGISTRY_HOSTPORT
|
||||
repo: nxtgauge-rust-${SERVICE}
|
||||
dockerfile: Dockerfile.binary
|
||||
build_args:
|
||||
- SERVICE_NAME=${SERVICE}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ steps:
|
|||
- name: build-and-push
|
||||
image: woodpeckerci/plugin-kaniko:2.1.1
|
||||
settings:
|
||||
registry: registry.nxtgauge.internal:5000
|
||||
registry:
|
||||
from_secret: REGISTRY_HOSTPORT
|
||||
repo: nxtgauge-rust-${SERVICE}
|
||||
dockerfile: Dockerfile.simple
|
||||
build_args:
|
||||
|
|
|
|||
18
.woodpecker/README.md
Normal file
18
.woodpecker/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Woodpecker CI Secrets
|
||||
|
||||
The following Woodpecker secrets are required for CI/CD pipelines:
|
||||
|
||||
| Secret Name | Purpose |
|
||||
| -------------------- | -------------------------------------------------------------- |
|
||||
| `REGISTRY_HOSTPORT` | Registry host:port (e.g., `registry.nxtgauge.com`) |
|
||||
| `REGISTRY_USERNAME` | Registry username for authentication |
|
||||
| `REGISTRY_PASSWORD` | Registry password/token for authentication |
|
||||
| `DOCKERHUB_USERNAME` | Docker Hub username (optional, for Docker Hub pushes) |
|
||||
| `DOCKERHUB_TOKEN` | Docker Hub access token (optional, for Docker Hub pushes) |
|
||||
| `GHCR_USERNAME` | GitHub Container Registry username (optional, for GHCR pushes) |
|
||||
| `GHCR_TOKEN` | GitHub Container Registry token (optional, for GHCR pushes) |
|
||||
| `GITOPS_REPO_URL` | GitOps repository URL (optional) |
|
||||
|
||||
## Usage
|
||||
|
||||
All build/push steps use these secrets via `from_secret:` references. No credentials are hardcoded in pipeline files.
|
||||
93
Cargo.lock
generated
93
Cargo.lock
generated
|
|
@ -788,6 +788,12 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
|
|
@ -1185,6 +1191,8 @@ dependencies = [
|
|||
"anyhow",
|
||||
"chrono",
|
||||
"lettre",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
@ -1510,9 +1518,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1814,6 +1824,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower-service",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2202,6 +2213,12 @@ dependencies = [
|
|||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "makeup_artists"
|
||||
version = "0.1.0"
|
||||
|
|
@ -2677,6 +2694,61 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.37",
|
||||
"socket2 0.5.10",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.37",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
|
|
@ -2868,6 +2940,8 @@ dependencies = [
|
|||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.37",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -2875,6 +2949,7 @@ dependencies = [
|
|||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
|
|
@ -2884,6 +2959,7 @@ dependencies = [
|
|||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2931,6 +3007,12 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
|
|
@ -2999,6 +3081,7 @@ version = "1.14.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
|
|
@ -4276,6 +4359,16 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
|
|
|
|||
|
|
@ -48,5 +48,6 @@ uuid = { version = "1", features = ["serde", "v4"] }
|
|||
chrono = { version = "0.4", features = ["serde"] }
|
||||
lettre = { version = "0.11", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder", "serde"] }
|
||||
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||
async-trait = "0.1"
|
||||
bytes = "1"
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ pub fn router() -> Router<AppState> {
|
|||
.route("/templates", get(list_templates))
|
||||
.route("/templates/:name/preview", get(preview_template))
|
||||
.route("/templates/:name/test", post(send_test_email))
|
||||
.route("/smtp-config", get(get_smtp_config).post(update_smtp_config))
|
||||
.route("/smtp-test", post(test_smtp_connection))
|
||||
.route("/email-config", get(get_email_config).post(update_email_config))
|
||||
.route("/email-test", post(test_email_connection))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -416,16 +416,21 @@ async fn send_test_email(
|
|||
}
|
||||
}
|
||||
|
||||
// ── SMTP Configuration ───────────────────────────────────────────────────────
|
||||
// ── Email Configuration ───────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SmtpConfig {
|
||||
host: String,
|
||||
port: i32,
|
||||
secure: bool,
|
||||
username: String,
|
||||
#[allow(dead_code)]
|
||||
struct EmailConfig {
|
||||
provider: String,
|
||||
smtp_host: String,
|
||||
smtp_port: i32,
|
||||
smtp_secure: bool,
|
||||
smtp_username: String,
|
||||
#[serde(skip_serializing)]
|
||||
password: Option<String>,
|
||||
smtp_password: Option<String>,
|
||||
zeptomail_api_key: String,
|
||||
zeptomail_from_email: String,
|
||||
zeptomail_from_name: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
reply_to_email: Option<String>,
|
||||
|
|
@ -433,65 +438,93 @@ struct SmtpConfig {
|
|||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SmtpConfigResponse {
|
||||
host: String,
|
||||
port: i32,
|
||||
secure: bool,
|
||||
username: String,
|
||||
struct EmailConfigResponse {
|
||||
provider: String,
|
||||
smtp_host: String,
|
||||
smtp_port: i32,
|
||||
smtp_secure: bool,
|
||||
smtp_username: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
reply_to_email: Option<String>,
|
||||
enabled: bool,
|
||||
zeptomail_configured: bool,
|
||||
}
|
||||
|
||||
async fn get_smtp_config() -> impl IntoResponse {
|
||||
// Return current SMTP configuration from environment
|
||||
let config = SmtpConfigResponse {
|
||||
host: std::env::var("SMTP_HOST").unwrap_or_default(),
|
||||
port: std::env::var("SMTP_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587),
|
||||
secure: std::env::var("SMTP_SECURE").unwrap_or_default().to_lowercase() == "true",
|
||||
username: std::env::var("SMTP_USER").unwrap_or_default(),
|
||||
from_email: std::env::var("SMTP_FROM_EMAIL").unwrap_or_else(|_| "noreply@nxtgauge.com".to_string()),
|
||||
from_name: std::env::var("SMTP_FROM_NAME").unwrap_or_else(|_| "NXTGAUGE".to_string()),
|
||||
reply_to_email: std::env::var("SMTP_REPLY_TO").ok(),
|
||||
enabled: std::env::var("SMTP_HOST").is_ok() && !std::env::var("SMTP_HOST").unwrap_or_default().is_empty(),
|
||||
async fn get_email_config() -> impl IntoResponse {
|
||||
let provider = std::env::var("EMAIL_PROVIDER").unwrap_or_else(|_| "SMTP".to_string());
|
||||
let zeptomail_configured = std::env::var("ZEPTOMAIL_API_KEY").is_ok();
|
||||
|
||||
let config = EmailConfigResponse {
|
||||
provider: provider.clone(),
|
||||
smtp_host: std::env::var("SMTP_HOST").unwrap_or_default(),
|
||||
smtp_port: std::env::var("SMTP_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587),
|
||||
smtp_secure: std::env::var("SMTP_SECURE").unwrap_or_default().to_lowercase() == "true",
|
||||
smtp_username: std::env::var("SMTP_USER").unwrap_or_default(),
|
||||
from_email: if provider == "ZEPTOMAIL" {
|
||||
std::env::var("ZEPTOMAIL_FROM_EMAIL").unwrap_or_else(|_| "noreply@nxtgauge.com".to_string())
|
||||
} else {
|
||||
std::env::var("SMTP_FROM_EMAIL").unwrap_or_else(|_| "noreply@nxtgauge.com".to_string())
|
||||
},
|
||||
from_name: if provider == "ZEPTOMAIL" {
|
||||
std::env::var("ZEPTOMAIL_FROM_NAME").unwrap_or_else(|_| "NXTGAUGE".to_string())
|
||||
} else {
|
||||
std::env::var("SMTP_FROM_NAME").unwrap_or_else(|_| "NXTGAUGE".to_string())
|
||||
},
|
||||
reply_to_email: std::env::var("SMTP_REPLY_TO")
|
||||
.ok()
|
||||
.or_else(|| std::env::var("ZEPTOMAIL_REPLY_TO").ok()),
|
||||
enabled: (provider == "SMTP" && std::env::var("SMTP_HOST").is_ok())
|
||||
|| (provider == "ZEPTOMAIL" && std::env::var("ZEPTOMAIL_API_KEY").is_ok()),
|
||||
zeptomail_configured,
|
||||
};
|
||||
|
||||
|
||||
(StatusCode::OK, Json(config))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdateSmtpConfigRequest {
|
||||
host: String,
|
||||
port: i32,
|
||||
secure: bool,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
struct UpdateEmailConfigRequest {
|
||||
provider: String,
|
||||
smtp_host: String,
|
||||
smtp_port: i32,
|
||||
smtp_secure: bool,
|
||||
smtp_username: String,
|
||||
smtp_password: Option<String>,
|
||||
zeptomail_api_key: String,
|
||||
zeptomail_from_email: String,
|
||||
zeptomail_from_name: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
reply_to_email: Option<String>,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
async fn update_smtp_config(
|
||||
Json(req): Json<UpdateSmtpConfigRequest>,
|
||||
async fn update_email_config(
|
||||
Json(req): Json<UpdateEmailConfigRequest>,
|
||||
) -> impl IntoResponse {
|
||||
// In production, this would update the database or secrets manager
|
||||
// For now, we just return success (env vars need restart to take effect)
|
||||
|
||||
if req.enabled && req.host.is_empty() {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||
"error": "SMTP host is required when enabled"
|
||||
})));
|
||||
if req.enabled {
|
||||
if req.provider == "SMTP" && req.smtp_host.is_empty() {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||
"error": "SMTP host is required when SMTP provider is enabled"
|
||||
})));
|
||||
}
|
||||
if req.provider == "ZEPTOMAIL" && req.zeptomail_api_key.is_empty() {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||
"error": "Zeptomail API key is required when Zeptomail provider is enabled"
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"message": "SMTP configuration updated. Restart services to apply changes.",
|
||||
"message": "Email configuration updated. Restart services to apply changes.",
|
||||
"config": {
|
||||
"host": req.host,
|
||||
"port": req.port,
|
||||
"secure": req.secure,
|
||||
"username": req.username,
|
||||
"provider": req.provider,
|
||||
"smtp_host": req.smtp_host,
|
||||
"smtp_port": req.smtp_port,
|
||||
"smtp_secure": req.smtp_secure,
|
||||
"smtp_username": req.smtp_username,
|
||||
"zeptomail_api_key": if req.zeptomail_api_key.is_empty() { "[hidden]".to_string() } else { "[configured]".to_string() },
|
||||
"from_email": req.from_email,
|
||||
"from_name": req.from_name,
|
||||
"reply_to_email": req.reply_to_email,
|
||||
|
|
@ -501,36 +534,33 @@ async fn update_smtp_config(
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SmtpTestRequest {
|
||||
struct EmailTestRequest {
|
||||
to_email: String,
|
||||
config: Option<SmtpTestConfig>,
|
||||
provider: Option<String>,
|
||||
config: Option<EmailTestConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SmtpTestConfig {
|
||||
host: String,
|
||||
port: i32,
|
||||
secure: bool,
|
||||
username: String,
|
||||
password: String,
|
||||
#[allow(dead_code)]
|
||||
struct EmailTestConfig {
|
||||
provider: String,
|
||||
smtp_host: String,
|
||||
smtp_port: i32,
|
||||
smtp_secure: bool,
|
||||
smtp_username: String,
|
||||
smtp_password: String,
|
||||
zeptomail_api_key: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
}
|
||||
|
||||
async fn test_smtp_connection(
|
||||
async fn test_email_connection(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<SmtpTestRequest>,
|
||||
Json(req): Json<EmailTestRequest>,
|
||||
) -> impl IntoResponse {
|
||||
// Send a test email using current or provided config
|
||||
let result = if let Some(test_config) = req.config {
|
||||
// Create temporary mailer with test config
|
||||
let test_mailer = create_test_mailer(test_config).await;
|
||||
test_mailer.send_test_email(&req.to_email).await
|
||||
} else {
|
||||
// Use existing mailer
|
||||
state.mail.send_test_email(&req.to_email).await
|
||||
};
|
||||
|
||||
let result = state.mail.send_test_email(&req.to_email).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => (StatusCode::OK, Json(serde_json::json!({
|
||||
"message": "Test email sent successfully",
|
||||
|
|
@ -541,9 +571,3 @@ async fn test_smtp_connection(
|
|||
}))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_test_mailer(config: SmtpTestConfig) -> email::Mailer {
|
||||
// This is a simplified version - in production you'd create a new Mailer instance
|
||||
// For now, we just return the default mailer
|
||||
email::Mailer::new()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,3 +8,5 @@ lettre = { workspace = true }
|
|||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -4,9 +4,98 @@ use lettre::{
|
|||
transport::smtp::authentication::Credentials,
|
||||
AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
use reqwest::Client;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum EmailProvider {
|
||||
Smtp(AsyncSmtpTransport<Tokio1Executor>),
|
||||
Zeptomail(ZeptomailTransport),
|
||||
}
|
||||
|
||||
pub struct ZeptomailTransport {
|
||||
client: Client,
|
||||
api_key: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl ZeptomailTransport {
|
||||
pub fn new(api_key: String, from_email: String, from_name: String) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_key,
|
||||
from_email,
|
||||
from_name,
|
||||
base_url: "https://api.zeptomail.com/v1.1/email".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&self, to: &str, subject: &str, html_body: &str) -> Result<()> {
|
||||
#[derive(Serialize)]
|
||||
struct ZeptomailRequest<'a> {
|
||||
from: ZeptomailAddress<'a>,
|
||||
to: Vec<ZeptomailAddress<'a>>,
|
||||
subject: &'a str,
|
||||
htmlbody: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ZeptomailAddress<'a> {
|
||||
address: &'a str,
|
||||
name: Option<&'a str>,
|
||||
}
|
||||
|
||||
let request = ZeptomailRequest {
|
||||
from: ZeptomailAddress {
|
||||
address: &self.from_email,
|
||||
name: Some(&self.from_name),
|
||||
},
|
||||
to: vec![ZeptomailAddress {
|
||||
address: to,
|
||||
name: None,
|
||||
}],
|
||||
subject,
|
||||
htmlbody: html_body,
|
||||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&self.base_url)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Zoho-enczapikey {}", self.api_key))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
tracing::info!("Zeptomail email sent successfully to {}", to);
|
||||
Ok(())
|
||||
} else {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
tracing::error!("Zeptomail send failed: {} - {}", status, body);
|
||||
Err(anyhow::anyhow!("Zeptomail send failed: {} - {}", status, body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ZeptomailTransport {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_key: self.api_key.clone(),
|
||||
from_email: self.from_email.clone(),
|
||||
from_name: self.from_name.clone(),
|
||||
base_url: self.base_url.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Template Engine ───────────────────────────────────────────────────────────
|
||||
|
||||
pub struct TemplateEngine;
|
||||
|
|
@ -103,7 +192,7 @@ impl Default for TemplateEngine {
|
|||
// ── Mailer ────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct Mailer {
|
||||
transport: Option<AsyncSmtpTransport<Tokio1Executor>>,
|
||||
provider: Option<EmailProvider>,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
template_engine: TemplateEngine,
|
||||
|
|
@ -111,42 +200,98 @@ pub struct Mailer {
|
|||
|
||||
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 provider_type = env::var("EMAIL_PROVIDER")
|
||||
.unwrap_or_else(|_| "SMTP".to_string())
|
||||
.to_uppercase();
|
||||
|
||||
let from_email = env::var("SMTP_FROM_EMAIL")
|
||||
.or_else(|_| env::var("ZEPTOMAIL_FROM_EMAIL".to_string()))
|
||||
.unwrap_or_else(|_| "noreply@nxtgauge.com".to_string());
|
||||
let from_name = env::var("SMTP_FROM_NAME")
|
||||
.or_else(|_| env::var("ZEPTOMAIL_FROM_NAME".to_string()))
|
||||
.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={} port={})", host, smtp_port);
|
||||
Some(t)
|
||||
let provider = match provider_type.as_str() {
|
||||
"ZEPTOMAIL_SMTP" | "ZEPTOMAIL" => {
|
||||
// Use Zeptomail via SMTP
|
||||
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);
|
||||
|
||||
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!("Zeptomail SMTP transport configured (host={} port={})", host, smtp_port);
|
||||
Some(EmailProvider::Smtp(t))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Zeptomail SMTP transport init failed: {} — emails disabled", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("SMTP transport init failed: {} — emails disabled", e);
|
||||
_ => {
|
||||
tracing::warn!("Zeptomail SMTP not configured — emails disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
"ZEPTOMAIL_API" => {
|
||||
// Use Zeptomail via HTTP API
|
||||
if let (Some(api_key), Some(from)) = (
|
||||
env::var("ZEPTOMAIL_API_KEY").ok(),
|
||||
env::var("ZEPTOMAIL_FROM_EMAIL").ok(),
|
||||
) {
|
||||
let transport = ZeptomailTransport::new(api_key, from.clone(), from_name.clone());
|
||||
tracing::info!("Zeptomail API transport configured (from={})", from);
|
||||
Some(EmailProvider::Zeptomail(transport))
|
||||
} else {
|
||||
tracing::warn!("Zeptomail API selected but not configured — emails disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("SMTP not configured — emails disabled");
|
||||
None
|
||||
// Default to SMTP
|
||||
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);
|
||||
|
||||
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={} port={})", host, smtp_port);
|
||||
Some(EmailProvider::Smtp(t))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("SMTP transport init failed: {} — emails disabled", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("SMTP not configured — emails disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
transport,
|
||||
provider,
|
||||
from_email,
|
||||
from_name,
|
||||
template_engine: TemplateEngine::new(),
|
||||
|
|
@ -154,22 +299,29 @@ impl Mailer {
|
|||
}
|
||||
|
||||
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);
|
||||
let Some(provider) = &self.provider else {
|
||||
tracing::debug!("No email provider configured — skipping email to {}", to);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let from: Mailbox = format!("{} <{}>", self.from_name, self.from_email).parse()?;
|
||||
let to: Mailbox = to.parse()?;
|
||||
match provider {
|
||||
EmailProvider::Smtp(transport) => {
|
||||
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)?;
|
||||
let email = Message::builder()
|
||||
.from(from)
|
||||
.to(to)
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html_body)?;
|
||||
|
||||
transport.send(email).await?;
|
||||
transport.send(email).await?;
|
||||
}
|
||||
EmailProvider::Zeptomail(transport) => {
|
||||
transport.send(to, subject, &html_body).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue