use emailks::{ config::AppConfig, email::EmailSender, etcd::{EtcdConfig, ServiceRegistry}, pb::email::v1::email_service_server::EmailServiceServer, queue::EmailQueue, server::EmailServiceImpl, }; use tonic::transport::Server; use tracing::{error, info}; const DEFAULT_QUEUE_CAPACITY: usize = 1_000; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info".into()), ) .init(); dotenvy::dotenv().ok(); // Phase 1: read etcd endpoints from env (required to bootstrap etcd) let etcd_endpoints: Vec = std::env::var("ETCD_ENDPOINTS") .unwrap_or_else(|_| "http://localhost:2379".to_string()) .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); let etcd_prefix = std::env::var("ETCD_KEY_PREFIX") .unwrap_or_else(|_| "/appks/".to_string()); // Phase 2: connect etcd, create config overlay (etcd > env > default) let etcd = EtcdConfig::connect(etcd_endpoints, &etcd_prefix).await?; let listen_addr_str = etcd.get("EMAILKS_LISTEN_ADDR", "127.0.0.1:50051").await; // Phase 3: register this service so other services (appks) can discover us let registry = ServiceRegistry::new(etcd.client(), &etcd_prefix); registry.register("emailks", &listen_addr_str).await?; // Phase 4: load SMTP config — each key: etcd first, then env, then default let smtp_host = etcd.get("APP_SMTP_HOST", "").await; if smtp_host.is_empty() { return Err("APP_SMTP_HOST is required (set via etcd or env)".into()); } let smtp_port: u16 = etcd.get_parsed("APP_SMTP_PORT", 587u16).await; let smtp_from_email = etcd.get("APP_SMTP_FROM_EMAIL", "").await; let smtp_from_name = etcd.get("APP_SMTP_FROM_NAME", "EmailKS").await; let smtp_reply_to = etcd.get("APP_SMTP_REPLY_TO", "").await; let smtp_tls = etcd.get("APP_SMTP_TLS", "starttls").await; let smtp_timeout_secs: u64 = etcd.get_parsed("APP_SMTP_TIMEOUT_SECS", 30u64).await; let smtp_allow_request_from: bool = etcd.get_parsed("APP_SMTP_ALLOW_REQUEST_FROM", false).await; let smtp_username = etcd.get("APP_SMTP_USERNAME", "").await; let smtp_password = etcd.get("APP_SMTP_PASSWORD", "").await; let smtp_helo_name = etcd.get("APP_SMTP_HELO_NAME", "").await; let queue_capacity: Option = { let s = etcd.get("APP_SMTP_QUEUE_CAPACITY", "").await; if s.is_empty() { None } else { s.parse().ok() } }; let config = AppConfig::from_etcd( smtp_host, smtp_port, smtp_username, smtp_password, smtp_from_email, smtp_from_name, smtp_reply_to, smtp_tls, smtp_timeout_secs, smtp_helo_name, smtp_allow_request_from, queue_capacity, &listen_addr_str, )?; info!(host = %config.smtp.host, port = config.smtp.port, "smtp config loaded (etcd priority)"); let sender = EmailSender::new(config.smtp)?; let (queue, worker) = match config.queue_capacity { Some(0) => { info!("creating unbounded queue"); EmailQueue::unbounded() } Some(cap) => { info!(capacity = cap, "creating bounded queue"); EmailQueue::bounded(cap) } None => { info!(capacity = DEFAULT_QUEUE_CAPACITY, "creating bounded queue (default)"); EmailQueue::bounded(DEFAULT_QUEUE_CAPACITY) } }; let store = queue.status_store().clone(); let worker_handle = worker.spawn(move |job| { let s = sender.clone(); async move { s.send_job(&job).await } }); let addr = config.listen_addr; let svc = EmailServiceImpl::new(queue, store); let (health, health_svc) = tonic_health::server::health_reporter(); health .set_serving::>() .await; info!(%addr, "gRPC server starting"); Server::builder() .add_service(health_svc) .add_service(EmailServiceServer::new(svc)) .serve_with_shutdown(addr, shutdown_signal()) .await?; info!("server stopped"); if let Err(e) = worker_handle.await { tracing::error!(error = %e, "worker task panicked"); } Ok(()) } async fn shutdown_signal() { match tokio::signal::ctrl_c().await { Ok(()) => info!("shutdown signal received"), Err(err) => error!(%err, "failed to install CTRL+C handler"), } }