Author: Kevin Schoon [me@kevinschoon.com]
Hash: 09fdb13cc30082d5439b0f821eb64a30e6c3edc4
Timestamp: Sun, 04 Aug 2024 12:31:30 +0000 (2 months ago)

+97 -88 +/-5 browse
refactor to not use explicit config structs
1diff --git a/cmd/maitred-debug/src/main.rs b/cmd/maitred-debug/src/main.rs
2index 2366043..b473484 100644
3--- a/cmd/maitred-debug/src/main.rs
4+++ b/cmd/maitred-debug/src/main.rs
5 @@ -11,7 +11,7 @@ async fn main() -> Result<(), Error> {
6 .init();
7
8 // Set the subscriber as the default subscriber
9- let mail_server = Server::new("localhost").with_address("127.0.0.1:2525");
10+ let mail_server = Server::new("localhost").address("127.0.0.1:2525");
11 mail_server.listen().await?;
12 Ok(())
13 }
14 diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs
15index 18eac88..a010ec5 100644
16--- a/maitred/src/lib.rs
17+++ b/maitred/src/lib.rs
18 @@ -5,7 +5,6 @@ mod session;
19 mod transport;
20
21 use smtp_proto::{Request, Response as SmtpResponse};
22- use transport::Response;
23
24 /// Low Level SMTP protocol is exported for convenience
25 pub use smtp_proto;
26 diff --git a/maitred/src/pipeline.rs b/maitred/src/pipeline.rs
27index 39c837d..750f90d 100644
28--- a/maitred/src/pipeline.rs
29+++ b/maitred/src/pipeline.rs
30 @@ -1,8 +1,6 @@
31- use std::result::Result as StdResult;
32-
33- use crate::session::{Result as SessionResult, Session};
34+ use crate::session::Result as SessionResult;
35 use crate::transport::Response;
36- use crate::{smtp_err, smtp_ok, Request, SmtpResponse};
37+ use crate::Request;
38
39 pub type Result = Vec<Response<String>>;
40 pub type Transaction = (Request<String>, SessionResult);
41 @@ -12,16 +10,9 @@ pub type Transaction = (Request<String>, SessionResult);
42 #[derive(Default)]
43 pub struct Pipeline {
44 history: Vec<Transaction>,
45- disable: bool,
46 }
47
48 impl Pipeline {
49- /// disable pipelining and return each each transaction transparently
50- pub fn disable(mut self) -> Self {
51- self.disable = true;
52- self
53- }
54-
55 /// Checks if the pipeline is within a data transaction (if the previous
56 /// command was DATA/BDAT).
57 fn within_tx(&self) -> bool {
58 @@ -168,7 +159,7 @@ impl Pipeline {
59 mod test {
60
61 use super::*;
62- use crate::{smtp_ok, Request, Response, SmtpResponse};
63+ use crate::{smtp_ok, Request, SmtpResponse};
64
65 #[test]
66 pub fn test_pipeline_basic() {
67 diff --git a/maitred/src/server.rs b/maitred/src/server.rs
68index d5441d3..303490b 100644
69--- a/maitred/src/server.rs
70+++ b/maitred/src/server.rs
71 @@ -41,15 +41,14 @@ const DEFAULT_CAPABILITIES: u32 =
72
73 /// Apply pipelining if running in extended mode and configured to support it
74 struct ConditionalPipeline<'a> {
75- pub opts: &'a SessionOptions,
76 pub session: &'a mut Session,
77 pub pipeline: &'a mut Pipeline,
78 }
79
80 impl ConditionalPipeline<'_> {
81 pub fn apply(&mut self, req: &Request<String>, data: Option<&Bytes>) -> Vec<Response<String>> {
82- let response = self.session.process(self.opts, req, data);
83- if self.opts.capabilities & smtp_proto::EXT_PIPELINING != 0 && self.session.is_extended() {
84+ let response = self.session.process(req, data);
85+ if self.session.has_capability(smtp_proto::EXT_PIPELINING) && self.session.is_extended() {
86 self.pipeline.process(req, &response)
87 } else {
88 match response {
89 @@ -103,44 +102,62 @@ impl Default for Configuration {
90 }
91
92 pub struct Server {
93- config: Configuration,
94+ address: String,
95+ hostname: String,
96+ greeting: String,
97+ global_timeout: Duration,
98+ help_banner: String,
99+ maximum_size: u64,
100+ capabilities: u32,
101+ }
102+
103+ impl Default for Server {
104+ fn default() -> Self {
105+ Server {
106+ address: DEFAULT_LISTEN_ADDR.to_string(),
107+ hostname: String::default(),
108+ greeting: DEFAULT_GREETING.to_string(),
109+ global_timeout: Duration::from_secs(DEFAULT_GLOBAL_TIMEOUT_SECS),
110+ help_banner: DEFAULT_HELP_BANNER.to_string(),
111+ maximum_size: DEFAULT_MAXIMUM_SIZE,
112+ capabilities: DEFAULT_CAPABILITIES,
113+ }
114+ }
115 }
116
117 impl Server {
118 /// Initialize a new SMTP server
119 pub fn new(hostname: &str) -> Self {
120 Server {
121- config: Configuration {
122- hostname: hostname.to_string(),
123- ..Default::default()
124- },
125+ hostname: hostname.to_string(),
126+ ..Default::default()
127 }
128 }
129
130 /// Greeting message returned from the server upon initial connection.
131- pub fn with_greeting(mut self, greeting: &str) -> Self {
132- self.config.greeting = greeting.to_string();
133+ pub fn greeting(mut self, greeting: &str) -> Self {
134+ self.greeting = greeting.to_string();
135 self
136 }
137
138 /// Listener address for the SMTP server to bind to listen for incoming
139 /// connections.
140- pub fn with_address(mut self, address: &str) -> Self {
141- self.config.address = address.to_string();
142+ pub fn address(mut self, address: &str) -> Self {
143+ self.address = address.to_string();
144 self
145 }
146
147 /// Set the maximum amount of time the server will wait for another command
148 /// before closing the connection. RFC states the suggested time is 5m.
149- pub fn with_timeout(mut self, timeout: Duration) -> Self {
150- self.config.global_timeout = timeout;
151+ pub fn timeout(mut self, timeout: Duration) -> Self {
152+ self.global_timeout = timeout;
153 self
154 }
155
156 /// Set the maximum size of a message which if exceeded will result in
157 /// rejection.
158- pub fn with_maximum_size(mut self, size: u64) -> Self {
159- self.config.maximum_size = size;
160+ pub fn maximum_size(mut self, size: u64) -> Self {
161+ self.maximum_size = size;
162 self
163 }
164
165 @@ -148,22 +165,22 @@ impl Server {
166 where
167 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
168 {
169- let mut session = Session::default();
170+ let mut session = Session::default()
171+ .capabilities(self.capabilities)
172+ .maximum_size(self.maximum_size)
173+ .our_hostname(&self.hostname)
174+ .help_banner(&self.help_banner);
175 let mut pipelined = ConditionalPipeline {
176- opts: &self.config.session_opts(),
177 session: &mut session,
178 pipeline: &mut Pipeline::default(),
179 };
180 // send inital server greeting
181 framed
182- .send(crate::session::greeting(
183- &self.config.hostname,
184- &self.config.greeting,
185- ))
186+ .send(crate::session::greeting(&self.hostname, &self.greeting))
187 .await?;
188
189 'outer: loop {
190- let frame = timeout(self.config.global_timeout, framed.next()).await;
191+ let frame = timeout(self.global_timeout, framed.next()).await;
192 match frame {
193 Ok(request) => {
194 if let Some(command) = request {
195 @@ -191,14 +208,11 @@ impl Server {
196 };
197 }
198 Err(timeout) => {
199- tracing::warn!(
200- "Client connection exceeded: {:?}",
201- self.config.global_timeout
202- );
203+ tracing::warn!("Client connection exceeded: {:?}", self.global_timeout);
204 framed
205 .send(crate::session::timeout(&timeout.to_string()))
206 .await?;
207- return Err(Error::Timeout(self.config.global_timeout.as_secs()));
208+ return Err(Error::Timeout(self.global_timeout.as_secs()));
209 }
210 }
211 }
212 @@ -207,8 +221,8 @@ impl Server {
213 }
214
215 pub async fn listen(&self) -> Result<(), Error> {
216- let listener = TcpListener::bind(&self.config.address).await?;
217- tracing::info!("Mail server listening @ {}", self.config.address);
218+ let listener = TcpListener::bind(&self.address).await?;
219+ tracing::info!("Mail server listening @ {}", self.address);
220 loop {
221 let (socket, _) = listener.accept().await.unwrap();
222 let addr = socket.local_addr()?;
223 diff --git a/maitred/src/session.rs b/maitred/src/session.rs
224index 88b7620..36e545e 100644
225--- a/maitred/src/session.rs
226+++ b/maitred/src/session.rs
227 @@ -34,14 +34,8 @@ pub fn timeout(message: &str) -> Response<String> {
228 smtp_response!(421, 4, 4, 2, format!("Timeout exceeded: {}", message))
229 }
230
231- /// Sent when the client attempts communication before initializing the
232- /// session with HELO/ELHO
233- pub fn not_initialized() -> Result {
234- smtp_err!(500, 0, 0, 0, "It's polite to say EHLO first")
235- }
236-
237 /// Runtime options that influence server behavior
238- #[derive(Default)]
239+ #[derive(Default, Clone)]
240 pub(crate) struct Options {
241 pub hostname: String,
242 /// Generic banner to show when the help command is sent without any
243 @@ -64,9 +58,35 @@ pub(crate) struct Session {
244 /// If an active data transfer is taking place
245 data_transfer: Option<DataTransfer>,
246 initialized: Option<Mode>,
247+ options: Options,
248+ // session options
249+ our_hostname: String,
250+ maximum_size: u64,
251+ capabilities: u32,
252+ help_banner: String,
253 }
254
255 impl Session {
256+ pub fn our_hostname(mut self, hostname: &str) -> Self {
257+ self.our_hostname = hostname.to_string();
258+ self
259+ }
260+
261+ pub fn maximum_size(mut self, maximum_size: u64) -> Self {
262+ self.maximum_size = maximum_size;
263+ self
264+ }
265+
266+ pub fn capabilities(mut self, capabilities: u32) -> Self {
267+ self.capabilities = capabilities;
268+ self
269+ }
270+
271+ pub fn help_banner(mut self, help_banner: &str) -> Self {
272+ self.help_banner = help_banner.to_string();
273+ self
274+ }
275+
276 pub fn reset(&mut self) {
277 self.body = None;
278 self.mail_from = None;
279 @@ -83,6 +103,12 @@ impl Session {
280 .is_some_and(|mode| matches!(mode, Mode::Extended))
281 }
282
283+ /// check if the capability is supported by the session
284+ pub fn has_capability(&self, capability: u32) -> bool {
285+ self.capabilities & capability != 0
286+ }
287+
288+ /// ensure that the session has been initialized otherwise return an error
289 fn check_initialized(&self) -> StdResult<(), Response<String>> {
290 if self.initialized.is_none() {
291 return Err(smtp_response!(
292 @@ -96,6 +122,11 @@ impl Session {
293 Ok(())
294 }
295
296+ /// checks if 8BITMIME is supported
297+ fn non_ascii_ok(&self) -> bool {
298+ false
299+ }
300+
301 /// Statefully process the SMTP command with optional data payload, any
302 /// error returned is passed back to the caller.
303 /// NOTE:
304 @@ -104,12 +135,7 @@ impl Session {
305 /// indicate that the process is starting and the second one contains the
306 /// parsed bytes from the transfer.
307 /// FIXME: Not at all reasonable yet
308- pub fn process(
309- &mut self,
310- opts: &Options,
311- req: &Request<String>,
312- data: Option<&Bytes>,
313- ) -> Result {
314+ pub fn process(&mut self, req: &Request<String>, data: Option<&Bytes>) -> Result {
315 match req {
316 Request::Ehlo { host } => {
317 self.hostname = Some(Host::parse(host).map_err(|e| {
318 @@ -117,8 +143,8 @@ impl Session {
319 })?);
320 self.initialized = Some(Mode::Extended);
321 let mut resp = EhloResponse::new(format!("Hello {}", host));
322- resp.capabilities = opts.capabilities;
323- resp.size = opts.maximum_size as usize;
324+ resp.capabilities = self.capabilities;
325+ resp.size = self.maximum_size as usize;
326 Ok(Response::Ehlo(resp))
327 }
328 Request::Lhlo { host } => {
329 @@ -210,7 +236,7 @@ impl Session {
330 Request::Help { value } => {
331 self.check_initialized()?;
332 if value.is_empty() {
333- smtp_ok!(250, 0, 0, 0, opts.help_banner.to_string())
334+ smtp_ok!(250, 0, 0, 0, self.help_banner.to_string())
335 } else {
336 smtp_ok!(
337 250,
338 @@ -290,10 +316,10 @@ mod test {
339 }
340
341 /// process all commands returning their response
342- fn process_all(session: &mut Session, opts: &Options, commands: &[TestCase]) {
343+ fn process_all(session: &mut Session, commands: &[TestCase]) {
344 commands.iter().enumerate().for_each(|(i, command)| {
345 println!("Running command {}/{}", i, commands.len());
346- let response = session.process(opts, &command.request, command.payload.as_ref());
347+ let response = session.process(&command.request, command.payload.as_ref());
348 println!("Response: {:?}", response);
349 match response {
350 Ok(actual_response) => {
351 @@ -350,14 +376,7 @@ mod test {
352 },
353 ];
354 let mut session = Session::default();
355- process_all(
356- &mut session,
357- &Options {
358- hostname: EXAMPLE_HOSTNAME.to_string(),
359- ..Default::default()
360- },
361- requests,
362- );
363+ process_all(&mut session, requests);
364 // session should contain both requests
365 assert!(session
366 .hostname
367 @@ -376,15 +395,8 @@ mod test {
368 payload: None,
369 expected: smtp_err!(500, 0, 0, 0, String::from("It's polite to say EHLO first")),
370 }];
371- let mut session = Session::default();
372- process_all(
373- &mut session,
374- &Options {
375- hostname: EXAMPLE_HOSTNAME.to_string(),
376- ..Default::default()
377- },
378- requests,
379- );
380+ let mut session = Session::default().our_hostname(EXAMPLE_HOSTNAME);
381+ process_all(&mut session, requests);
382 }
383
384 #[test]
385 @@ -444,15 +456,8 @@ transport rather than the session.
386 expected: smtp_ok!(250, 0, 0, 0, "OK"),
387 },
388 ];
389- let mut session = Session::default();
390- process_all(
391- &mut session,
392- &Options {
393- hostname: EXAMPLE_HOSTNAME.to_string(),
394- ..Default::default()
395- },
396- requests,
397- );
398+ let mut session = Session::default().our_hostname(EXAMPLE_HOSTNAME);
399+ process_all(&mut session, requests);
400 assert!(session
401 .mail_from
402 .is_some_and(|mail_from| mail_from.get_email() == "fuu@example.org"));