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
|
- name: build-base-image
|
||||||
image: woodpeckerci/plugin-docker-buildx:5.0.0
|
image: woodpeckerci/plugin-docker-buildx:5.0.0
|
||||||
settings:
|
settings:
|
||||||
registry: ghcr.io
|
registry:
|
||||||
repo: ghcr.io/traceworks2023/nxtgauge-rust-base
|
from_secret: REGISTRY_HOSTPORT
|
||||||
|
repo: nxtgauge-rust-base
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.base
|
dockerfile: Dockerfile.base
|
||||||
tags:
|
tags:
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,9 @@ steps:
|
||||||
- name: build-docker
|
- name: build-docker
|
||||||
image: woodpeckerci/plugin-docker-buildx:5.0.0
|
image: woodpeckerci/plugin-docker-buildx:5.0.0
|
||||||
settings:
|
settings:
|
||||||
registry: docker.io
|
registry:
|
||||||
repo: your-dockerhub-username/nxtgauge-rust-${SERVICE}
|
from_secret: REGISTRY_HOSTPORT
|
||||||
|
repo: nxtgauge-rust-${SERVICE}
|
||||||
dockerfile: Dockerfile.binary
|
dockerfile: Dockerfile.binary
|
||||||
build_args:
|
build_args:
|
||||||
- SERVICE_NAME=${SERVICE}
|
- SERVICE_NAME=${SERVICE}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ steps:
|
||||||
- name: build-and-push
|
- name: build-and-push
|
||||||
image: woodpeckerci/plugin-kaniko:2.1.1
|
image: woodpeckerci/plugin-kaniko:2.1.1
|
||||||
settings:
|
settings:
|
||||||
registry: registry.nxtgauge.internal:5000
|
registry:
|
||||||
|
from_secret: REGISTRY_HOSTPORT
|
||||||
repo: nxtgauge-rust-${SERVICE}
|
repo: nxtgauge-rust-${SERVICE}
|
||||||
dockerfile: Dockerfile.simple
|
dockerfile: Dockerfile.simple
|
||||||
build_args:
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.44"
|
version = "0.4.44"
|
||||||
|
|
@ -1185,6 +1191,8 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"lettre",
|
"lettre",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1510,9 +1518,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 5.3.0",
|
"r-efi 5.3.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1814,6 +1824,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.26.4",
|
"tokio-rustls 0.26.4",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"webpki-roots 1.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2202,6 +2213,12 @@ dependencies = [
|
||||||
"hashbrown 0.15.5",
|
"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]]
|
[[package]]
|
||||||
name = "makeup_artists"
|
name = "makeup_artists"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -2677,6 +2694,61 @@ dependencies = [
|
||||||
"cc",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
|
|
@ -2868,6 +2940,8 @@ dependencies = [
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls 0.23.37",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -2875,6 +2949,7 @@ dependencies = [
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
"tokio-rustls 0.26.4",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
|
@ -2884,6 +2959,7 @@ dependencies = [
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"webpki-roots 1.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2931,6 +3007,12 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -2999,6 +3081,7 @@ version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4276,6 +4359,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.26.11"
|
version = "0.26.11"
|
||||||
|
|
|
||||||
|
|
@ -48,5 +48,6 @@ uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
lettre = { version = "0.11", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder", "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"] }
|
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
|
||||||
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ pub fn router() -> Router<AppState> {
|
||||||
.route("/templates", get(list_templates))
|
.route("/templates", get(list_templates))
|
||||||
.route("/templates/:name/preview", get(preview_template))
|
.route("/templates/:name/preview", get(preview_template))
|
||||||
.route("/templates/:name/test", post(send_test_email))
|
.route("/templates/:name/test", post(send_test_email))
|
||||||
.route("/smtp-config", get(get_smtp_config).post(update_smtp_config))
|
.route("/email-config", get(get_email_config).post(update_email_config))
|
||||||
.route("/smtp-test", post(test_smtp_connection))
|
.route("/email-test", post(test_email_connection))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -416,16 +416,21 @@ async fn send_test_email(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SMTP Configuration ───────────────────────────────────────────────────────
|
// ── Email Configuration ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct SmtpConfig {
|
#[allow(dead_code)]
|
||||||
host: String,
|
struct EmailConfig {
|
||||||
port: i32,
|
provider: String,
|
||||||
secure: bool,
|
smtp_host: String,
|
||||||
username: String,
|
smtp_port: i32,
|
||||||
|
smtp_secure: bool,
|
||||||
|
smtp_username: String,
|
||||||
#[serde(skip_serializing)]
|
#[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_email: String,
|
||||||
from_name: String,
|
from_name: String,
|
||||||
reply_to_email: Option<String>,
|
reply_to_email: Option<String>,
|
||||||
|
|
@ -433,65 +438,93 @@ struct SmtpConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct SmtpConfigResponse {
|
struct EmailConfigResponse {
|
||||||
host: String,
|
provider: String,
|
||||||
port: i32,
|
smtp_host: String,
|
||||||
secure: bool,
|
smtp_port: i32,
|
||||||
username: String,
|
smtp_secure: bool,
|
||||||
|
smtp_username: String,
|
||||||
from_email: String,
|
from_email: String,
|
||||||
from_name: String,
|
from_name: String,
|
||||||
reply_to_email: Option<String>,
|
reply_to_email: Option<String>,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
zeptomail_configured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_smtp_config() -> impl IntoResponse {
|
async fn get_email_config() -> impl IntoResponse {
|
||||||
// Return current SMTP configuration from environment
|
let provider = std::env::var("EMAIL_PROVIDER").unwrap_or_else(|_| "SMTP".to_string());
|
||||||
let config = SmtpConfigResponse {
|
let zeptomail_configured = std::env::var("ZEPTOMAIL_API_KEY").is_ok();
|
||||||
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),
|
let config = EmailConfigResponse {
|
||||||
secure: std::env::var("SMTP_SECURE").unwrap_or_default().to_lowercase() == "true",
|
provider: provider.clone(),
|
||||||
username: std::env::var("SMTP_USER").unwrap_or_default(),
|
smtp_host: std::env::var("SMTP_HOST").unwrap_or_default(),
|
||||||
from_email: std::env::var("SMTP_FROM_EMAIL").unwrap_or_else(|_| "noreply@nxtgauge.com".to_string()),
|
smtp_port: std::env::var("SMTP_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587),
|
||||||
from_name: std::env::var("SMTP_FROM_NAME").unwrap_or_else(|_| "NXTGAUGE".to_string()),
|
smtp_secure: std::env::var("SMTP_SECURE").unwrap_or_default().to_lowercase() == "true",
|
||||||
reply_to_email: std::env::var("SMTP_REPLY_TO").ok(),
|
smtp_username: std::env::var("SMTP_USER").unwrap_or_default(),
|
||||||
enabled: std::env::var("SMTP_HOST").is_ok() && !std::env::var("SMTP_HOST").unwrap_or_default().is_empty(),
|
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))
|
(StatusCode::OK, Json(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct UpdateSmtpConfigRequest {
|
#[allow(dead_code)]
|
||||||
host: String,
|
struct UpdateEmailConfigRequest {
|
||||||
port: i32,
|
provider: String,
|
||||||
secure: bool,
|
smtp_host: String,
|
||||||
username: String,
|
smtp_port: i32,
|
||||||
password: Option<String>,
|
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_email: String,
|
||||||
from_name: String,
|
from_name: String,
|
||||||
reply_to_email: Option<String>,
|
reply_to_email: Option<String>,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_smtp_config(
|
async fn update_email_config(
|
||||||
Json(req): Json<UpdateSmtpConfigRequest>,
|
Json(req): Json<UpdateEmailConfigRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// In production, this would update the database or secrets manager
|
if req.enabled {
|
||||||
// For now, we just return success (env vars need restart to take effect)
|
if req.provider == "SMTP" && req.smtp_host.is_empty() {
|
||||||
|
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||||
if req.enabled && req.host.is_empty() {
|
"error": "SMTP host is required when SMTP provider is enabled"
|
||||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
})));
|
||||||
"error": "SMTP host is required when 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!({
|
(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": {
|
"config": {
|
||||||
"host": req.host,
|
"provider": req.provider,
|
||||||
"port": req.port,
|
"smtp_host": req.smtp_host,
|
||||||
"secure": req.secure,
|
"smtp_port": req.smtp_port,
|
||||||
"username": req.username,
|
"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_email": req.from_email,
|
||||||
"from_name": req.from_name,
|
"from_name": req.from_name,
|
||||||
"reply_to_email": req.reply_to_email,
|
"reply_to_email": req.reply_to_email,
|
||||||
|
|
@ -501,36 +534,33 @@ async fn update_smtp_config(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SmtpTestRequest {
|
struct EmailTestRequest {
|
||||||
to_email: String,
|
to_email: String,
|
||||||
config: Option<SmtpTestConfig>,
|
provider: Option<String>,
|
||||||
|
config: Option<EmailTestConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SmtpTestConfig {
|
#[allow(dead_code)]
|
||||||
host: String,
|
struct EmailTestConfig {
|
||||||
port: i32,
|
provider: String,
|
||||||
secure: bool,
|
smtp_host: String,
|
||||||
username: String,
|
smtp_port: i32,
|
||||||
password: String,
|
smtp_secure: bool,
|
||||||
|
smtp_username: String,
|
||||||
|
smtp_password: String,
|
||||||
|
zeptomail_api_key: String,
|
||||||
from_email: String,
|
from_email: String,
|
||||||
from_name: String,
|
from_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_smtp_connection(
|
async fn test_email_connection(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<SmtpTestRequest>,
|
Json(req): Json<EmailTestRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Send a test email using current or provided config
|
// Send a test email using current or provided config
|
||||||
let result = if let Some(test_config) = req.config {
|
let result = state.mail.send_test_email(&req.to_email).await;
|
||||||
// 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
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => (StatusCode::OK, Json(serde_json::json!({
|
Ok(_) => (StatusCode::OK, Json(serde_json::json!({
|
||||||
"message": "Test email sent successfully",
|
"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 }
|
anyhow = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,98 @@ use lettre::{
|
||||||
transport::smtp::authentication::Credentials,
|
transport::smtp::authentication::Credentials,
|
||||||
AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
|
AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
|
||||||
};
|
};
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
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 ───────────────────────────────────────────────────────────
|
// ── Template Engine ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct TemplateEngine;
|
pub struct TemplateEngine;
|
||||||
|
|
@ -103,7 +192,7 @@ impl Default for TemplateEngine {
|
||||||
// ── Mailer ────────────────────────────────────────────────────────────────────
|
// ── Mailer ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct Mailer {
|
pub struct Mailer {
|
||||||
transport: Option<AsyncSmtpTransport<Tokio1Executor>>,
|
provider: Option<EmailProvider>,
|
||||||
from_email: String,
|
from_email: String,
|
||||||
from_name: String,
|
from_name: String,
|
||||||
template_engine: TemplateEngine,
|
template_engine: TemplateEngine,
|
||||||
|
|
@ -111,42 +200,98 @@ pub struct Mailer {
|
||||||
|
|
||||||
impl Mailer {
|
impl Mailer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let smtp_host = env::var("SMTP_HOST").ok();
|
let provider_type = env::var("EMAIL_PROVIDER")
|
||||||
let smtp_user = env::var("SMTP_USER").ok();
|
.unwrap_or_else(|_| "SMTP".to_string())
|
||||||
let smtp_pass = env::var("SMTP_PASS").ok();
|
.to_uppercase();
|
||||||
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")
|
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());
|
.unwrap_or_else(|_| "noreply@nxtgauge.com".to_string());
|
||||||
let from_name = env::var("SMTP_FROM_NAME")
|
let from_name = env::var("SMTP_FROM_NAME")
|
||||||
|
.or_else(|_| env::var("ZEPTOMAIL_FROM_NAME".to_string()))
|
||||||
.unwrap_or_else(|_| "NXTGAUGE".to_string());
|
.unwrap_or_else(|_| "NXTGAUGE".to_string());
|
||||||
|
|
||||||
let transport = match (smtp_host, smtp_user, smtp_pass) {
|
let provider = match provider_type.as_str() {
|
||||||
(Some(host), Some(user), Some(pass)) => {
|
"ZEPTOMAIL_SMTP" | "ZEPTOMAIL" => {
|
||||||
let creds = Credentials::new(user, pass);
|
// Use Zeptomail via SMTP
|
||||||
match AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&host) {
|
let smtp_host = env::var("SMTP_HOST").ok();
|
||||||
Ok(builder) => {
|
let smtp_user = env::var("SMTP_USER").ok();
|
||||||
let t = builder.port(smtp_port).credentials(creds).build();
|
let smtp_pass = env::var("SMTP_PASS").ok();
|
||||||
tracing::info!("SMTP transport configured (host={} port={})", host, smtp_port);
|
let smtp_port: u16 = env::var("SMTP_PORT")
|
||||||
Some(t)
|
.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
|
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");
|
// Default to SMTP
|
||||||
None
|
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 {
|
Self {
|
||||||
transport,
|
provider,
|
||||||
from_email,
|
from_email,
|
||||||
from_name,
|
from_name,
|
||||||
template_engine: TemplateEngine::new(),
|
template_engine: TemplateEngine::new(),
|
||||||
|
|
@ -154,22 +299,29 @@ impl Mailer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_html(&self, to: &str, subject: &str, html_body: String) -> Result<()> {
|
async fn send_html(&self, to: &str, subject: &str, html_body: String) -> Result<()> {
|
||||||
let Some(transport) = &self.transport else {
|
let Some(provider) = &self.provider else {
|
||||||
tracing::debug!("SMTP disabled — skipping email to {}", to);
|
tracing::debug!("No email provider configured — skipping email to {}", to);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let from: Mailbox = format!("{} <{}>", self.from_name, self.from_email).parse()?;
|
match provider {
|
||||||
let to: Mailbox = to.parse()?;
|
EmailProvider::Smtp(transport) => {
|
||||||
|
let from: Mailbox = format!("{} <{}>", self.from_name, self.from_email).parse()?;
|
||||||
|
let to: Mailbox = to.parse()?;
|
||||||
|
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from(from)
|
.from(from)
|
||||||
.to(to)
|
.to(to)
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.header(ContentType::TEXT_HTML)
|
.header(ContentType::TEXT_HTML)
|
||||||
.body(html_body)?;
|
.body(html_body)?;
|
||||||
|
|
||||||
transport.send(email).await?;
|
transport.send(email).await?;
|
||||||
|
}
|
||||||
|
EmailProvider::Zeptomail(transport) => {
|
||||||
|
transport.send(to, subject, &html_body).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue