Author: Kevin Schoon [me@kevinschoon.com]
Hash: a67b8dcaa5222908824bd85f0c090374a9b0e287
Timestamp: Tue, 03 Sep 2024 08:02:53 +0000 (1 month ago)

+67 -41 +/-5 browse
wire up spf validation; refactor
1diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs
2index 18658ff..9188bd4 100644
3--- a/maitred/src/lib.rs
4+++ b/maitred/src/lib.rs
5 @@ -62,6 +62,7 @@ mod rewrite;
6 mod server;
7 mod session;
8 mod transport;
9+ mod validation;
10 mod worker;
11
12 use smtp_proto::Response as SmtpResponse;
13 diff --git a/maitred/src/server.rs b/maitred/src/server.rs
14index e7a90db..77ce634 100644
15--- a/maitred/src/server.rs
16+++ b/maitred/src/server.rs
17 @@ -7,7 +7,6 @@ use crossbeam_deque::Stealer;
18 use crossbeam_deque::Worker as WorkQueue;
19 use futures::SinkExt;
20 use futures::StreamExt;
21- use mail_auth::Resolver;
22 use smtp_proto::Request;
23 use tokio::net::TcpListener;
24 use tokio::sync::mpsc::Sender;
25 @@ -23,7 +22,7 @@ use crate::milter::Milter;
26 use crate::session::{Session, SessionOptions};
27 use crate::smtp_response;
28 use crate::transport::{Command, Transport, TransportError};
29- use crate::worker::AuthSystem;
30+ use crate::validation::Validation;
31 use crate::worker::{Packet, Worker};
32 use crate::{Response, SmtpResponse};
33
34 @@ -244,7 +243,7 @@ impl Server {
35 Ok(())
36 }
37
38- async fn spawn_workers(&mut self, global_queue: Arc<Injector<Packet>>) {
39+ async fn spawn_workers(&mut self, global_queue: Arc<Injector<Packet>>, validation: &Validation) {
40 let local_queues: Vec<WorkQueue<Packet>> = (0..self.n_threads)
41 .map(|_| WorkQueue::<Packet>::new_fifo())
42 .collect();
43 @@ -261,10 +260,8 @@ impl Server {
44 let stealers = stealers.clone();
45 let milter = self.milter.clone();
46 let delivery = self.delivery.clone();
47- let dkim_verification = self.dkim_verification;
48+ let thread_validation = validation.clone();
49 tokio::task::spawn(async move {
50- let mut auth = AuthSystem::new().await;
51- auth.dkim_verification = dkim_verification;
52 let mut worker = Worker {
53 milter,
54 delivery,
55 @@ -272,7 +269,7 @@ impl Server {
56 stealers,
57 local_queue: Arc::new(Mutex::new(local_queue)),
58 shutdown_rx,
59- auth,
60+ validation: thread_validation,
61 };
62 worker.process().await
63 })
64 @@ -298,7 +295,9 @@ impl Server {
65 let listener = TcpListener::bind(&self.address).await?;
66 tracing::info!("Mail server listening @ {}", self.address);
67 let global_queue = Arc::new(Injector::<Packet>::new());
68- self.spawn_workers(global_queue.clone()).await;
69+ let mut validation = Validation::new().await;
70+ validation.dkim_enabled = self.dkim_verification;
71+ self.spawn_workers(global_queue.clone(), &validation).await;
72 loop {
73 let (socket, _) = listener.accept().await.unwrap();
74 let addr = socket.local_addr()?;
75 @@ -309,7 +308,6 @@ impl Server {
76 .is_some_and(|opts| opts.capabilities & smtp_proto::EXT_PIPELINING != 0)
77 || self.options.is_none();
78 let framed = Framed::new(socket, Transport::default().pipelining(pipelining));
79-
80 match self.process(framed, global_queue.clone()).await {
81 Ok(_) => {
82 tracing::info!("Client connection finished normally");
83 diff --git a/maitred/src/session.rs b/maitred/src/session.rs
84index c5486b7..f1ae928 100644
85--- a/maitred/src/session.rs
86+++ b/maitred/src/session.rs
87 @@ -14,6 +14,7 @@ use crate::auth::{AuthData, PlainAuth};
88 use crate::expand::Expansion;
89 use crate::smtp_response;
90 use crate::transport::Response;
91+ use crate::validation::Validation;
92 use crate::verify::Verify;
93
94 /// Default help banner returned from a HELP command without any parameters
95 @@ -89,6 +90,7 @@ pub struct SessionOptions {
96 pub list_expansion: Option<Arc<dyn Expansion>>,
97 pub verification: Option<Arc<dyn Verify>>,
98 pub plain_auth: Option<Arc<dyn PlainAuth>>,
99+ pub validation: Option<Validation>,
100 }
101
102 impl Default for SessionOptions {
103 @@ -102,6 +104,7 @@ impl Default for SessionOptions {
104 list_expansion: None,
105 verification: None,
106 plain_auth: None,
107+ validation: None,
108 }
109 }
110 }
111 diff --git a/maitred/src/validation.rs b/maitred/src/validation.rs
112new file mode 100644
113index 0000000..b9d2ed2
114--- /dev/null
115+++ b/maitred/src/validation.rs
116 @@ -0,0 +1,50 @@
117+ use std::net::IpAddr;
118+
119+ use mail_auth::{AuthenticatedMessage, DkimResult, Resolver};
120+
121+ /// Wraps around mail_auth to do various email authentication / validation
122+ #[derive(Clone)]
123+ pub struct Validation {
124+ resolver: Resolver,
125+ pub dkim_enabled: bool,
126+ pub spf_enabled: bool,
127+ }
128+
129+ impl Validation {
130+ pub async fn new() -> Self {
131+ let resolver = Resolver::new_system_conf().unwrap();
132+ Validation {
133+ resolver,
134+ dkim_enabled: false,
135+ spf_enabled: false,
136+ }
137+ }
138+
139+ pub async fn verify_dkim(&self, message: &[u8]) -> bool {
140+ let authenticated = AuthenticatedMessage::parse(message).unwrap();
141+ let passed = self
142+ .resolver
143+ .verify_dkim(&authenticated)
144+ .await
145+ .iter()
146+ .all(|s| {
147+ tracing::info!("{:?}", s);
148+ matches!(s.result(), DkimResult::Pass)
149+ });
150+ passed
151+ }
152+
153+ pub async fn verify_spf(
154+ &self,
155+ ip: IpAddr,
156+ helo_domain: &str,
157+ host_domain: &str,
158+ sender: &str,
159+ ) -> bool {
160+ let output = self
161+ .resolver
162+ .verify_spf_sender(ip, helo_domain, host_domain, sender)
163+ .await;
164+ matches!(output.result(), mail_auth::SpfResult::Pass)
165+ }
166+ }
167 diff --git a/maitred/src/worker.rs b/maitred/src/worker.rs
168index 78bda79..2e55725 100644
169--- a/maitred/src/worker.rs
170+++ b/maitred/src/worker.rs
171 @@ -3,7 +3,7 @@ use std::{iter, time::Duration};
172
173 use crossbeam_deque::{Injector, Stealer, Worker as WorkQueue};
174 use email_address::EmailAddress;
175- use mail_auth::{AuthenticatedMessage, DkimResult, Resolver};
176+ use mail_auth::{AuthenticatedMessage, DkimResult};
177 use mail_parser::Message;
178 use tokio::sync::{mpsc::Receiver, Mutex};
179 use url::Host;
180 @@ -12,25 +12,11 @@ use crate::delivery::Delivery;
181 use crate::milter::Milter;
182 use crate::rewrite::Rewrite;
183 use crate::session::Session;
184+ use crate::validation::Validation;
185 use crate::Error;
186
187 const HEADER_DKIM_RESULT: &str = "Maitred-Dkim-Result";
188
189- pub struct AuthSystem {
190- resolver: Resolver,
191- pub dkim_verification: bool,
192- }
193-
194- impl AuthSystem {
195- pub async fn new() -> Self {
196- let resolver = Resolver::new_system_conf().unwrap();
197- AuthSystem {
198- resolver,
199- dkim_verification: false,
200- }
201- }
202- }
203-
204 /// Session details to be passed internally for processing
205 #[derive(Clone, Debug)]
206 pub(crate) struct Packet {
207 @@ -65,7 +51,7 @@ pub(crate) struct Worker {
208 pub stealers: Vec<Stealer<Packet>>,
209 pub local_queue: Arc<Mutex<WorkQueue<Packet>>>,
210 pub shutdown_rx: Receiver<bool>,
211- pub auth: AuthSystem,
212+ pub validation: Validation,
213 }
214
215 impl Worker {
216 @@ -95,29 +81,17 @@ impl Worker {
217
218 if let Some(packet) = self.next_packet().await {
219 let mut message = packet.body.unwrap();
220+ let message_bytes = message.raw_message();
221 let message_id = message
222 .message_id()
223 .map(|id| id.to_string())
224 .unwrap_or(String::from("Unknown Message"));
225- let message_bytes = message.raw_message().to_vec();
226 let mut dkim_passed: Option<bool> = None;
227- if self.auth.dkim_verification {
228- let authenticated =
229- AuthenticatedMessage::parse(message_bytes.as_slice()).unwrap();
230+ if self.validation.dkim_enabled {
231 tracing::info!("DKIM Verification for {}", message_id);
232- let passed = self
233- .auth
234- .resolver
235- .verify_dkim(&authenticated)
236- .await
237- .iter()
238- .all(|s| {
239- tracing::info!("{:?}", s);
240- matches!(s.result(), DkimResult::Pass)
241- });
242+ let passed = self.validation.verify_dkim(&message_bytes).await;
243 dkim_passed = Some(passed);
244 }
245-
246 if let Some(milter) = &self.milter {
247 tracing::info!("Applying Milter to message {}", message_id);
248 match milter.apply(&message).await {