Commit
+97 -88 +/-5 browse
1 | diff --git a/cmd/maitred-debug/src/main.rs b/cmd/maitred-debug/src/main.rs |
2 | index 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 |
15 | index 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 |
27 | index 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 |
68 | index 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 |
224 | index 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")); |