pub mod config; pub mod email; pub mod email_build; pub mod error; pub mod etcd; pub mod queue; pub mod server; pub mod status; pub(crate) const ENV_PREFIX: &str = "APP_SMTP_"; pub mod pb { pub mod email { pub mod v1 { include!(concat!(env!("OUT_DIR"), "/pb/email.v1.rs")); } } } use std::future::Future; use std::net::SocketAddr; use crate::config::AppConfig; use crate::email::EmailSender; use crate::pb::email::v1::email_service_server::EmailServiceServer; use crate::queue::EmailQueue; use crate::server::EmailServiceImpl; use tonic::transport::Server as TonicServer; use tracing::{error, info}; const DEFAULT_QUEUE_CAPACITY: usize = 1_000; /// A ready-to-run emailks gRPC server. /// /// Use [`EmailksServerBuilder`] to construct an instance from configuration. pub struct EmailksServer { service: EmailServiceImpl, worker_handle: tokio::task::JoinHandle<()>, addr: SocketAddr, } /// Builder for [`EmailksServer`]. /// /// # Examples /// /// ```no_run /// use emailks::{EmailksServer, config::AppConfig}; /// /// # async fn example() -> Result<(), Box> { /// let config = AppConfig::from_env()?; /// let server = EmailksServer::builder() /// .config(config) /// .build() /// .await?; /// server.serve().await?; /// # Ok(()) /// # } /// ``` pub struct EmailksServerBuilder { config: Option, } impl EmailksServer { /// Create a new builder. pub fn builder() -> EmailksServerBuilder { EmailksServerBuilder::default() } /// Start the gRPC server and block until Ctrl+C (or SIGTERM on Unix). pub async fn serve(self) -> Result<(), Box> { self.serve_with_shutdown(shutdown_signal()).await } /// Start the gRPC server and block until the provided `shutdown` future resolves. /// /// In-flight requests are drained before the server returns. pub async fn serve_with_shutdown( self, shutdown: impl Future, ) -> Result<(), Box> { let svc = self.service; let addr = self.addr; let (health, health_svc) = tonic_health::server::health_reporter(); health .set_serving::>() .await; info!(%addr, "gRPC server starting"); TonicServer::builder() .add_service(health_svc) .add_service(EmailServiceServer::new(svc)) .serve_with_shutdown(addr, shutdown) .await?; info!("server stopped"); if let Err(e) = self.worker_handle.await { error!(error = %e, "worker task panicked"); } Ok(()) } } impl EmailksServerBuilder { /// Set the server configuration. pub fn config(mut self, config: AppConfig) -> Self { self.config = Some(config); self } /// Build the server, creating the SMTP sender, queue, worker, and gRPC service. pub async fn build(self) -> Result> { let config = self .config .unwrap_or_else(|| AppConfig::from_env().expect("failed to load emailks config")); info!( host = %config.smtp.host, port = config.smtp.port, "smtp config loaded" ); 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 service = EmailServiceImpl::new(queue, store); Ok(EmailksServer { service, worker_handle, addr, }) } } impl Default for EmailksServerBuilder { fn default() -> Self { Self { config: None } } } 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"), } }