mail_laser/lib.rs
1//! Orchestrates the MailLaser application startup and component lifecycle.
2//!
3//! This library crate initializes configuration and concurrently runs the primary
4//! services (SMTP, Health Check). It ensures that if any essential service
5//! terminates unexpectedly, the entire application will shut down gracefully.
6
7pub mod smtp;
8pub mod webhook;
9pub mod config;
10pub mod health;
11
12use anyhow::Result;
13use log::{info, error};
14use tokio::select;
15
16/// Runs the main MailLaser application logic.
17///
18/// Initializes and launches the SMTP and health check servers in separate asynchronous tasks.
19/// It then monitors these tasks using `tokio::select!`. The application is designed to run
20/// indefinitely. This function will only return if a critical error occurs in configuration
21/// loading or if one of the essential server tasks terminates unexpectedly (either by
22/// error, panic, or unexpected clean exit).
23///
24/// # Returns
25///
26/// - `Ok(())`: Should theoretically never return this in normal operation, as servers run indefinitely.
27/// - `Err(anyhow::Error)`: If configuration loading fails, or if either the SMTP or health
28/// check server task stops unexpectedly. The error indicates a fatal condition preventing
29/// the application from continuing.
30pub async fn run() -> Result<()> {
31 info!(
32 "Starting {} v{} inbound-SMTP server",
33 env!("CARGO_PKG_NAME"),
34 env!("CARGO_PKG_VERSION")
35 );
36
37 // Load configuration; exit early if configuration is invalid or missing.
38 let config = match config::Config::from_env() {
39 Ok(config) => config,
40 Err(e) => {
41 error!("Failed to load configuration: {}", e);
42 return Err(e); // Propagate configuration error to main.rs for process exit.
43 }
44 };
45
46 let smtp_server = smtp::Server::new(config.clone());
47 // Clone config for the health server task, as each task needs its own owned copy.
48 let health_config = config.clone();
49
50 // Spawn the health check server task.
51 let health_handle = tokio::spawn(async move {
52 if let Err(e) = health::run_health_server(health_config).await {
53 error!("Health check server encountered a fatal error: {}", e);
54 Err(e) // Propagate the error to the select! macro.
55 } else {
56 // A server task exiting without error is unexpected for a long-running service.
57 Ok(()) // Signal this unexpected state to select! for error handling.
58 }
59 });
60
61 // Spawn the main SMTP server task.
62 let smtp_handle = tokio::spawn(async move {
63 if let Err(e) = smtp_server.run().await {
64 error!("SMTP server encountered a fatal error: {}", e);
65 Err(e) // Propagate the error to the select! macro.
66 } else {
67 // A server task exiting without error is unexpected for a long-running service.
68 Ok(()) // Signal this unexpected state to select! for error handling.
69 }
70 });
71
72 // Monitor both server tasks concurrently. `select!` waits for the first task to complete.
73 // For long-running services, completion usually indicates an issue.
74 select! {
75 // `res` is Result<Result<()>, JoinError>
76 // Outer Ok: Task finished normally (returned Ok or Err).
77 // Outer Err: Task panicked or was cancelled.
78 // Inner Ok: Task function returned Ok(()).
79 // Inner Err: Task function returned an Err.
80 res = health_handle => {
81 error!("Health check server task terminated.");
82 match res {
83 Ok(Ok(())) => {
84 // Task completed without returning an error. This is unexpected for a
85 // persistent server, so we treat it as an application error.
86 Err(anyhow::anyhow!("Health check server exited cleanly, which is unexpected."))
87 }
88 Ok(Err(e)) => {
89 // Task completed and returned a specific error. Propagate it.
90 error!("Health check server returned error: {}", e);
91 Err(e)
92 }
93 Err(join_error) => {
94 // Task panicked or was cancelled. Wrap the JoinError.
95 error!("Health check server task failed (panic or cancellation): {}", join_error);
96 Err(anyhow::anyhow!("Health check server task failed: {}", join_error))
97 }
98 }
99 },
100 res = smtp_handle => {
101 error!("SMTP server task terminated.");
102 match res {
103 Ok(Ok(())) => {
104 // Task completed without returning an error. Unexpected for the main server.
105 Err(anyhow::anyhow!("SMTP server exited cleanly, which is unexpected."))
106 }
107 Ok(Err(e)) => {
108 // Task completed and returned a specific error. Propagate it.
109 error!("SMTP server returned error: {}", e);
110 Err(e)
111 }
112 Err(join_error) => {
113 // Task panicked or was cancelled. Wrap the JoinError.
114 error!("SMTP server task failed (panic or cancellation): {}", join_error);
115 Err(anyhow::anyhow!("SMTP server task failed: {}", join_error))
116 }
117 }
118 },
119 }
120 // The Result (Ok or Err) from the completed task's branch in select! is returned.
121 // Control should ideally not reach *past* the select! block in this setup.
122}