mail_laser/webhook/mod.rs
1//! Handles sending processed email data to a configured webhook URL via HTTPS POST.
2//!
3//! This module defines the data structure for the webhook payload (`EmailPayload`)
4//! and provides a `WebhookClient` responsible for making the asynchronous HTTP request.
5//! It uses `hyper` and `hyper-rustls` for the underlying HTTP/S communication.
6
7use anyhow::Result;
8use hyper::Request;
9use hyper_rustls::HttpsConnectorBuilder;
10// Import necessary components from hyper-util, using aliases for clarity.
11use hyper_util::{client::legacy::{connect::HttpConnector, Client}, rt::TokioExecutor};
12use http_body_util::Full; // For creating simple, complete request bodies.
13use bytes::Bytes; // Bytes type for request body data.
14use log::{info, error};
15use serde::{Serialize, Deserialize};
16use crate::config::Config;
17
18// --- Type Aliases for Hyper Client ---
19
20/// Type alias for the HTTPS connector using `hyper-rustls`.
21type HttpsConn = hyper_rustls::HttpsConnector<HttpConnector>;
22/// Type alias for the specific Hyper client configuration used for sending webhooks.
23/// Uses the `HttpsConn` for TLS and expects/sends `Full<Bytes>` bodies.
24type WebhookHttpClient = Client<HttpsConn, Full<Bytes>>;
25
26// --- Public Data Structures ---
27
28/// Represents the data payload sent to the webhook URL.
29///
30/// Contains the essential extracted information from a received email.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct EmailPayload {
33 /// The email address of the original sender.
34 pub sender: String,
35 /// The specific recipient address this email was accepted for.
36 pub recipient: String,
37 /// The subject line of the email.
38 pub subject: String,
39 /// The plain text representation of the body (HTML stripped).
40 pub body: String,
41 /// The original HTML body content, if the email contained HTML.
42 #[serde(skip_serializing_if = "Option::is_none")] // Don't include in JSON if None
43 pub html_body: Option<String>,
44}
45
46/// A client responsible for sending `EmailPayload` data to a configured webhook URL.
47///
48/// Encapsulates the `hyper` HTTP client setup with `rustls` for HTTPS support.
49pub struct WebhookClient {
50 /// Shared application configuration.
51 config: Config,
52 /// The underlying asynchronous HTTP client instance.
53 client: WebhookHttpClient,
54 /// The User-Agent string sent with webhook requests, derived from the crate's metadata.
55 user_agent: String,
56}
57
58impl WebhookClient {
59 /// Creates a new `WebhookClient`.
60 ///
61 /// Initializes an HTTPS client using `hyper-rustls` with native system certificates.
62 /// Constructs a User-Agent string based on the crate's name and version from `Cargo.toml`.
63 ///
64 /// # Arguments
65 ///
66 /// * `config` - The application configuration, used to get the webhook URL.
67 ///
68 /// # Panics
69 ///
70 /// Panics if loading the system's native root TLS certificates fails. This is considered
71 /// a fatal error during startup.
72 pub fn new(config: Config) -> Self {
73 // Configure the HTTPS connector using rustls and native certs.
74 let https = HttpsConnectorBuilder::new()
75 .with_native_roots()
76 // Panic if cert loading fails - essential for HTTPS operation.
77 .expect("Failed to load native root certificates for hyper-rustls")
78 .https_only() // Ensure only HTTPS connections are made.
79 .enable_http1() // Enable HTTP/1.1 support.
80 .build();
81
82 // Build the hyper client using the HTTPS connector and Tokio runtime.
83 let client: WebhookHttpClient = Client::builder(TokioExecutor::new()).build(https);
84
85 // Create a User-Agent string like "MailLaser/0.1.0".
86 let user_agent = format!(
87 "{}/{}",
88 env!("CARGO_PKG_NAME"),
89 env!("CARGO_PKG_VERSION")
90 );
91
92 Self {
93 config,
94 client,
95 user_agent,
96 }
97 }
98
99 /// Sends the given `EmailPayload` to the configured webhook URL.
100 ///
101 /// Serializes the payload to JSON and sends it as an HTTPS POST request.
102 /// Logs the outcome (success or failure status code) of the request.
103 ///
104 /// **Note:** A non-successful HTTP status code from the webhook endpoint (e.g., 4xx, 5xx)
105 /// is logged as an error but does *not* cause this function to return an `Err`.
106 /// The email is considered successfully processed by MailLaser once the webhook
107 /// request is attempted.
108 ///
109 /// # Arguments
110 ///
111 /// * `email` - The `EmailPayload` to send.
112 ///
113 /// # Errors
114 ///
115 /// Returns an `Err` if:
116 /// - Serialization of the `EmailPayload` to JSON fails.
117 /// - Building the HTTP request fails.
118 /// - The HTTP request itself fails (e.g., network error, DNS resolution failure).
119 pub async fn forward_email(&self, email: EmailPayload) -> Result<()> {
120 info!("Forwarding email from {} with subject: {}", email.sender, email.subject);
121
122 // Serialize payload to JSON. This can fail if the payload is invalid (unlikely here).
123 let json_body = serde_json::to_string(&email)?;
124
125 // Build the POST request.
126 let request = Request::builder()
127 .method(hyper::Method::POST)
128 .uri(&self.config.webhook_url) // Target URL from config.
129 .header("content-type", "application/json") // Set JSON content type.
130 .header("user-agent", &self.user_agent) // Set the custom User-Agent.
131 // Create the request body from the serialized JSON string.
132 .body(Full::new(Bytes::from(json_body)))?; // This can fail if headers/URI are invalid.
133
134 // Send the request asynchronously using the hyper client.
135 let response = self.client.request(request).await?;
136
137 // Check the HTTP status code of the response.
138 let status = response.status();
139 if !status.is_success() {
140 // Log webhook failures but don't propagate the error, as per design.
141 error!(
142 "Webhook request to {} failed with status: {}",
143 self.config.webhook_url, status
144 );
145 } else {
146 info!(
147 "Email successfully forwarded to webhook {}, status: {}",
148 self.config.webhook_url, status
149 );
150 }
151
152 // Return Ok regardless of the webhook's response status code.
153 Ok(())
154 }
155}