Skip to main content

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}