use std::sync::Arc;
use axum::{extract::State, http::{HeaderMap, StatusCode}, Json};
use serde_json::{json, Value};
use crate::{
auth::AuthContext,
error::AppError,
mail, smtp, validation,
AppState,
};
pub use crate::validation::MailRequest;
pub async fn send_mail(
State(state): State<Arc<AppState>>,
req_headers: HeaderMap,
auth: AuthContext,
Json(payload): Json<MailRequest>,
) -> Result<(StatusCode, Json<Value>), AppError> {
state.rate_limiter.check_global().map_err(|e| {
tracing::warn!(event = "rate_limited", tier = "global", retry_after = e.retry_after_secs);
AppError::RateLimited { retry_after_secs: Some(e.retry_after_secs) }
})?;
state.rate_limiter.check_ip(auth.client_ip).map_err(|e| {
tracing::warn!(event = "rate_limited", tier = "ip", client_ip = %auth.client_ip, retry_after = e.retry_after_secs);
AppError::RateLimited { retry_after_secs: Some(e.retry_after_secs) }
})?;
state
.rate_limiter
.check_key(&auth.key_id, auth.key_rate_limit_per_min, auth.key_burst)
.map_err(|e| {
tracing::warn!(
event = "rate_limited",
tier = "key",
key_id = %auth.key_id,
retry_after = e.retry_after_secs,
);
AppError::RateLimited {
retry_after_secs: Some(e.retry_after_secs),
}
})?;
let cfg = state.config();
let validated = validation::validate_mail_request(payload, &cfg, &auth)
.map_err(|e| {
tracing::warn!(
event = "validation_failure",
key_id = %auth.key_id,
client_ip = %auth.client_ip,
error = %e,
);
e
})?;
let message = mail::build_message(&validated, &cfg)?;
let recipient_domain = validated.to.first()
.and_then(|a| a.split('@').nth(1))
.unwrap_or("unknown");
let smtp_result = if cfg.smtp.mode == "pipe" {
smtp::submit_pipe(
message,
&cfg.smtp.pipe_command,
cfg.smtp.submission_timeout_seconds,
).await
} else {
smtp::submit(&state.smtp, message, cfg.smtp.submission_timeout_seconds).await
};
smtp_result.map_err(|e| {
tracing::error!(
event = "smtp_failure",
key_id = %auth.key_id,
client_ip = %auth.client_ip,
recipient_domain = recipient_domain,
error = %e,
);
e
})?;
tracing::info!(
event = "smtp_submitted",
key_id = %auth.key_id,
recipient_domain = recipient_domain,
);
let request_id = req_headers
.get("x-internal-request-id")
.and_then(|v| v.to_str().ok())
.map(String::from)
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
Ok((
StatusCode::ACCEPTED,
Json(json!({
"request_id": request_id,
"status": "accepted",
})),
))
}