Commit
+679 -39 +/-7 browse
1 | diff --git a/cmd/maitred-debug/src/main.rs b/cmd/maitred-debug/src/main.rs |
2 | index b4db71a..2366043 100644 |
3 | --- a/cmd/maitred-debug/src/main.rs |
4 | +++ b/cmd/maitred-debug/src/main.rs |
5 | @@ -1,4 +1,4 @@ |
6 | - use maitred::{Builder, Error}; |
7 | + use maitred::{Error, Server}; |
8 | use tracing::Level; |
9 | |
10 | #[tokio::main] |
11 | @@ -11,7 +11,7 @@ async fn main() -> Result<(), Error> { |
12 | .init(); |
13 | |
14 | // Set the subscriber as the default subscriber |
15 | - let mail_server = Builder::default().listen("127.0.0.1:2525").build(); |
16 | + let mail_server = Server::new("localhost").with_address("127.0.0.1:2525"); |
17 | mail_server.listen().await?; |
18 | Ok(()) |
19 | } |
20 | diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs |
21 | index e11ad6a..a642e9f 100644 |
22 | --- a/maitred/src/lib.rs |
23 | +++ b/maitred/src/lib.rs |
24 | @@ -4,4 +4,4 @@ mod session; |
25 | mod transport; |
26 | |
27 | pub use error::Error; |
28 | - pub use server::{Builder, Server}; |
29 | + pub use server::Server; |
30 | diff --git a/maitred/src/server.rs b/maitred/src/server.rs |
31 | index ccc7fb6..f1ee7e7 100644 |
32 | --- a/maitred/src/server.rs |
33 | +++ b/maitred/src/server.rs |
34 | @@ -8,35 +8,64 @@ use crate::error::Error; |
35 | use crate::session::Session; |
36 | use crate::transport::Transport; |
37 | |
38 | - pub struct Server { |
39 | - addr: String, |
40 | - } |
41 | + const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:2525"; |
42 | + const DEFAULT_GREETING: &str = "Maitred ESMTP Server"; |
43 | |
44 | - #[derive(Default)] |
45 | - pub struct Builder { |
46 | - addr: String, |
47 | + #[derive(Clone)] |
48 | + struct Configuration { |
49 | + pub address: String, |
50 | + pub hostname: String, |
51 | + pub greeting: String, |
52 | } |
53 | |
54 | - impl Builder { |
55 | - pub fn listen(mut self, addr: &str) -> Builder { |
56 | - self.addr = addr.to_string(); |
57 | - self |
58 | + impl Default for Configuration { |
59 | + fn default() -> Self { |
60 | + Configuration { |
61 | + address: DEFAULT_LISTEN_ADDR.to_string(), |
62 | + hostname: String::default(), |
63 | + greeting: DEFAULT_GREETING.to_string(), |
64 | + } |
65 | } |
66 | + } |
67 | + |
68 | + pub struct Server { |
69 | + config: Configuration, |
70 | + } |
71 | |
72 | - pub fn build(self) -> Server { |
73 | + impl Server { |
74 | + /// Initialize a new SMTP server |
75 | + pub fn new(hostname: &str) -> Self { |
76 | Server { |
77 | - addr: self.addr.clone(), |
78 | + config: Configuration { |
79 | + hostname: hostname.to_string(), |
80 | + ..Default::default() |
81 | + }, |
82 | } |
83 | } |
84 | - } |
85 | |
86 | - impl Server { |
87 | + /// Listener address for the SMTP server to bind to listen for incoming |
88 | + /// connections. |
89 | + pub fn with_address(mut self, address: &str) -> Self { |
90 | + self.config.address = address.to_string(); |
91 | + self |
92 | + } |
93 | + |
94 | async fn process(&self, stream: TcpStream) -> Result<(), Error> { |
95 | let peer = stream.peer_addr()?; |
96 | tracing::info!("Processing new TCP connection from {:?}", peer); |
97 | let transport = Transport::default(); |
98 | let mut framed = Framed::new(stream, transport); |
99 | let mut session = Session::default(); |
100 | + let session_opts = crate::session::Options { |
101 | + hostname: self.config.hostname.clone(), |
102 | + }; |
103 | + // send inital server greeting |
104 | + framed |
105 | + .send(crate::session::greeting( |
106 | + &self.config.hostname, |
107 | + &self.config.greeting, |
108 | + )) |
109 | + .await?; |
110 | let mut finished = false; |
111 | while let Some(request) = framed.next().await { |
112 | match request { |
113 | @@ -45,7 +74,7 @@ impl Server { |
114 | finished = true; |
115 | } |
116 | |
117 | - match session.process(&command.0, command.1) { |
118 | + match session.process(&session_opts, &command.0, command.1) { |
119 | Ok(resp) => { |
120 | tracing::debug!("Returning response: {:?}", resp); |
121 | framed.send(resp).await?; |
122 | @@ -70,8 +99,8 @@ impl Server { |
123 | } |
124 | |
125 | pub async fn listen(&self) -> Result<(), Error> { |
126 | - let listener = TcpListener::bind(&self.addr).await?; |
127 | - tracing::info!("Mail server listening @ {}", self.addr); |
128 | + let listener = TcpListener::bind(&self.config.address).await?; |
129 | + tracing::info!("Mail server listening @ {}", self.config.address); |
130 | loop { |
131 | let (socket, _) = listener.accept().await.unwrap(); |
132 | // TODO: timeout |
133 | diff --git a/maitred/src/session.rs b/maitred/src/session.rs |
134 | index 44a50be..af82e3b 100644 |
135 | --- a/maitred/src/session.rs |
136 | +++ b/maitred/src/session.rs |
137 | @@ -1,3 +1,5 @@ |
138 | + use std::result::Result as StdResult; |
139 | + |
140 | use bytes::Bytes; |
141 | use mail_parser::{Addr, Message, MessageParser}; |
142 | use melib::Address; |
143 | @@ -7,22 +9,45 @@ use url::Host; |
144 | use crate::error::Error; |
145 | use crate::transport::Command; |
146 | |
147 | - /// State of an active SMTP session |
148 | + pub type Result = StdResult<Response<String>, Response<String>>; |
149 | + |
150 | + enum DataTransfer { |
151 | + Data, |
152 | + Bdat, |
153 | + } |
154 | + |
155 | + /// A greeting must be sent at the start of an SMTP connection when it is |
156 | + /// first initialized. |
157 | + pub fn greeting(hostname: &str, greeting: &str) -> Response<String> { |
158 | + Response::new(220, 2, 0, 0, format!("{} {}", hostname, greeting)) |
159 | + } |
160 | + |
161 | + /// Runtime options that influence server behavior |
162 | + #[derive(Default)] |
163 | + pub(crate) struct Options { |
164 | + pub hostname: String, |
165 | + } |
166 | + |
167 | + /// Stateful connection that coresponds to a single SMTP session |
168 | #[derive(Default)] |
169 | - pub(crate) struct Session<'a> { |
170 | + pub(crate) struct Session { |
171 | /// all previous commands excluding |
172 | pub history: Vec<Request<String>>, |
173 | /// message body |
174 | - pub body: Option<Message<'a>>, |
175 | + pub body: Option<Vec<u8>>, |
176 | /// mailto address |
177 | pub mail_to: Option<Address>, |
178 | /// rcpt address |
179 | pub rcpt_to: Option<Address>, |
180 | pub hostname: Option<Host>, |
181 | + |
182 | + /// If an active data transfer is taking place |
183 | + data_transfer: Option<DataTransfer>, |
184 | } |
185 | |
186 | - impl Session<'_> { |
187 | + impl Session { |
188 | pub fn reset(&mut self) { |
189 | + self.history.clear(); |
190 | self.body = None; |
191 | self.mail_to = None; |
192 | self.rcpt_to = None; |
193 | @@ -31,12 +56,18 @@ impl Session<'_> { |
194 | |
195 | /// Statefully process the SMTP command with optional data payload, any |
196 | /// error returned is passed back to the caller. |
197 | + /// NOTE: |
198 | + /// Data transfers are detected in the transport level and handled by two |
199 | + /// calls to process(). The first call contains the empty data request to |
200 | + /// indicate that the process is starting and the second one contains the |
201 | + /// parsed bytes from the transfer. |
202 | /// FIXME: Not at all reasonable yet |
203 | pub fn process( |
204 | &mut self, |
205 | + opts: &Options, |
206 | req: &Request<String>, |
207 | data: Option<Bytes>, |
208 | - ) -> Result<Response<String>, Response<String>> { |
209 | + ) -> Result { |
210 | self.history.push(req.clone()); |
211 | match req { |
212 | Request::Ehlo { host } => { |
213 | @@ -68,24 +99,54 @@ impl Session<'_> { |
214 | ) |
215 | })?; |
216 | self.mail_to = Some(mail_to.clone()); |
217 | - Ok(Response::new(250, 0, 0, 0, mail_to.to_string())) |
218 | + Ok(Response::new(250, 0, 0, 0, "OK".to_string())) |
219 | } |
220 | Request::Rcpt { to } => { |
221 | let mail_to = Address::try_from(to.address.as_str()).map_err(|e| { |
222 | Response::new(500, 0, 0, 0, format!("Cannot parse: {} {}", to.address, e)) |
223 | })?; |
224 | self.mail_to = Some(mail_to.clone()); |
225 | - Ok(Response::new(250, 0, 0, 0, mail_to.to_string())) |
226 | + Ok(Response::new(250, 0, 0, 0, "OK".to_string())) |
227 | } |
228 | Request::Bdat { |
229 | chunk_size: _, |
230 | is_last: _, |
231 | } => { |
232 | - let message_payload = data.expect("data returned without a payload").to_vec(); |
233 | - let parser = MessageParser::new(); |
234 | - let result = parser.parse(&message_payload); |
235 | - tracing::info!("Parsed message successfully: {:?}", result); |
236 | - Ok(Response::new(200, 0, 0, 0, "Message processed".to_string())) |
237 | + if let Some(transfer_mode) = &self.data_transfer { |
238 | + match transfer_mode { |
239 | + DataTransfer::Data => { |
240 | + panic!("Transfer mode changed from DATA to BDAT") |
241 | + } |
242 | + DataTransfer::Bdat => { |
243 | + let message_payload = |
244 | + data.expect("data returned without a payload").to_vec(); |
245 | + let parser = MessageParser::new(); |
246 | + let response = match parser.parse(&message_payload) { |
247 | + Some(_) => Ok(Response::new(250, 0, 0, 0, "OK".to_string())), |
248 | + None => Err(Response::new( |
249 | + 500, |
250 | + 0, |
251 | + 0, |
252 | + 0, |
253 | + "Cannot parse message payload".to_string(), |
254 | + )), |
255 | + }?; |
256 | + self.data_transfer = None; |
257 | + self.body = Some(message_payload.clone()); |
258 | + Ok(response) |
259 | + } |
260 | + } |
261 | + } else { |
262 | + tracing::info!("Initializing data transfer mode"); |
263 | + self.data_transfer = Some(DataTransfer::Bdat); |
264 | + Ok(Response::new( |
265 | + 354, |
266 | + 0, |
267 | + 0, |
268 | + 0, |
269 | + "Starting BDAT data transfer".to_string(), |
270 | + )) |
271 | + } |
272 | } |
273 | Request::Auth { |
274 | mechanism, |
275 | @@ -100,11 +161,41 @@ impl Session<'_> { |
276 | Request::Burl { uri, is_last } => todo!(), |
277 | Request::StartTls => todo!(), |
278 | Request::Data => { |
279 | - let message_payload = data.expect("data returned without a payload").to_vec(); |
280 | - let parser = MessageParser::new(); |
281 | - let result = parser.parse(&message_payload); |
282 | - tracing::info!("Parsed message successfully: {:?}", result); |
283 | - Ok(Response::new(200, 0, 0, 0, "Message processed".to_string())) |
284 | + if let Some(transfer_mode) = &self.data_transfer { |
285 | + match transfer_mode { |
286 | + DataTransfer::Bdat => { |
287 | + panic!("Transfer mode changed from BDAT to DATA") |
288 | + } |
289 | + DataTransfer::Data => { |
290 | + let message_payload = |
291 | + data.expect("data returned without a payload").to_vec(); |
292 | + let parser = MessageParser::new(); |
293 | + let response = match parser.parse(&message_payload) { |
294 | + Some(_) => Ok(Response::new(250, 0, 0, 0, "OK".to_string())), |
295 | + None => Err(Response::new( |
296 | + 500, |
297 | + 0, |
298 | + 0, |
299 | + 0, |
300 | + "Cannot parse message payload".to_string(), |
301 | + )), |
302 | + }?; |
303 | + self.data_transfer = None; |
304 | + self.body = Some(message_payload.clone()); |
305 | + Ok(response) |
306 | + } |
307 | + } |
308 | + } else { |
309 | + tracing::info!("Initializing data transfer mode"); |
310 | + self.data_transfer = Some(DataTransfer::Data); |
311 | + Ok(Response::new( |
312 | + 354, |
313 | + 0, |
314 | + 0, |
315 | + 0, |
316 | + "Reading data input, end the message with <CRLF>.<CRLF>".to_string(), |
317 | + )) |
318 | + } |
319 | } |
320 | Request::Rset => { |
321 | self.reset(); |
322 | @@ -114,3 +205,178 @@ impl Session<'_> { |
323 | } |
324 | } |
325 | } |
326 | + |
327 | + #[cfg(test)] |
328 | + mod test { |
329 | + use super::*; |
330 | + |
331 | + use smtp_proto::{MailFrom, RcptTo}; |
332 | + |
333 | + const EXAMPLE_HOSTNAME: &str = "example.org"; |
334 | + |
335 | + struct TestCase { |
336 | + pub request: Request<String>, |
337 | + pub payload: Option<Bytes>, |
338 | + pub expected: Result, |
339 | + } |
340 | + |
341 | + /// process all commands returning their response |
342 | + fn process_all(session: &mut Session, opts: &Options, commands: &[TestCase]) { |
343 | + commands.iter().enumerate().for_each(|(i, command)| { |
344 | + println!("Running command {}/{}", i, commands.len()); |
345 | + let response = session.process(opts, &command.request, command.payload.clone()); |
346 | + println!("Response: {:?}", response); |
347 | + match response { |
348 | + Ok(actual_response) => { |
349 | + assert!(actual_response.code < 400); |
350 | + match &command.expected { |
351 | + Ok(expected_response) => { |
352 | + if !actual_response.eq(expected_response) { |
353 | + panic!( |
354 | + "Unexpected response:\n\nActual: {:?}\nExpected: {:?}\n", |
355 | + actual_response, expected_response |
356 | + ); |
357 | + } |
358 | + } |
359 | + Err(expected_err) => { |
360 | + panic!( |
361 | + "Expected an error but got valid response:\n\nResponse: {:?}\nExpected Error: {:?}", |
362 | + actual_response, expected_err |
363 | + ); |
364 | + }, |
365 | + } |
366 | + } |
367 | + Err(actual_err) => { |
368 | + match &command.expected { |
369 | + Ok(response) => { |
370 | + panic!( |
371 | + "Expected a valid response but got error:\nExpected: {:?}\nError: {:?}", |
372 | + response, actual_err, |
373 | + ); |
374 | + }, |
375 | + Err(expected_err) => { |
376 | + assert!(actual_err.code > 300); |
377 | + if !actual_err.eq(expected_err) { |
378 | + panic!("Expected error does not match:\n\nActual: {:?}\n Expected: {:?}", actual_err, expected_err); |
379 | + } |
380 | + }, |
381 | + } |
382 | + } |
383 | + } |
384 | + }) |
385 | + } |
386 | + |
387 | + #[test] |
388 | + fn test_hello_quit() { |
389 | + let requests = &[ |
390 | + TestCase { |
391 | + request: Request::Helo { |
392 | + host: EXAMPLE_HOSTNAME.to_string(), |
393 | + }, |
394 | + payload: None, |
395 | + expected: Ok(Response::new( |
396 | + 250, |
397 | + 0, |
398 | + 0, |
399 | + 0, |
400 | + String::from("Hello example.org"), |
401 | + )), |
402 | + }, |
403 | + TestCase { |
404 | + request: Request::Quit {}, |
405 | + payload: None, |
406 | + expected: Ok(Response::new(221, 0, 0, 0, String::from("Ciao!"))), |
407 | + }, |
408 | + ]; |
409 | + let mut session = Session::default(); |
410 | + process_all( |
411 | + &mut session, |
412 | + &Options { |
413 | + hostname: EXAMPLE_HOSTNAME.to_string(), |
414 | + }, |
415 | + requests, |
416 | + ); |
417 | + // session should contain both requests |
418 | + assert!(session.history.len() == 2); |
419 | + } |
420 | + |
421 | + #[test] |
422 | + fn test_email_with_body() { |
423 | + let requests = &[ |
424 | + TestCase { |
425 | + request: Request::Helo { |
426 | + host: EXAMPLE_HOSTNAME.to_string(), |
427 | + }, |
428 | + payload: None, |
429 | + expected: Ok(Response::new( |
430 | + 250, |
431 | + 0, |
432 | + 0, |
433 | + 0, |
434 | + String::from("Hello example.org"), |
435 | + )), |
436 | + }, |
437 | + TestCase { |
438 | + request: Request::Mail { |
439 | + from: MailFrom { |
440 | + address: String::from("fuu@example.org"), |
441 | + ..Default::default() |
442 | + }, |
443 | + }, |
444 | + payload: None, |
445 | + expected: Ok(Response::new(250, 0, 0, 0, String::from("OK"))), |
446 | + }, |
447 | + TestCase { |
448 | + request: Request::Rcpt { |
449 | + to: RcptTo { |
450 | + address: String::from("fuu@example.org"), |
451 | + ..Default::default() |
452 | + }, |
453 | + }, |
454 | + payload: None, |
455 | + expected: Ok(Response::new(250, 0, 0, 0, String::from("OK"))), |
456 | + }, |
457 | + // initiate data transfer |
458 | + TestCase { |
459 | + request: Request::Data {}, |
460 | + payload: None, |
461 | + expected: Ok(Response::new( |
462 | + 354, |
463 | + 0, |
464 | + 0, |
465 | + 0, |
466 | + String::from("Reading data input, end the message with <CRLF>.<CRLF>"), |
467 | + )), |
468 | + }, |
469 | + // send the actual payload |
470 | + TestCase { |
471 | + request: Request::Data {}, |
472 | + payload: Some(Bytes::from_static( |
473 | + br#"Subject: Hello World |
474 | + |
475 | + This is an e-mail from a test case! |
476 | + |
477 | + Note that it doesn't end with a "." since that parsing happens as part of the |
478 | + transport rather than the session. |
479 | + "#, |
480 | + )), |
481 | + expected: Ok(Response::new(250, 0, 0, 0, String::from("OK"))), |
482 | + }, |
483 | + ]; |
484 | + let mut session = Session::default(); |
485 | + process_all( |
486 | + &mut session, |
487 | + &Options { |
488 | + hostname: EXAMPLE_HOSTNAME.to_string(), |
489 | + }, |
490 | + requests, |
491 | + ); |
492 | + assert!(session.history.len() == 5); |
493 | + assert!(session.body.is_some_and(|body| { |
494 | + let message = MessageParser::new().parse(&body).unwrap(); |
495 | + message |
496 | + .subject() |
497 | + .is_some_and(|subject| subject == "Hello World") |
498 | + })); |
499 | + } |
500 | + } |
501 | diff --git a/maitred/src/transport.rs b/maitred/src/transport.rs |
502 | index fa2dabe..f0f1425 100644 |
503 | --- a/maitred/src/transport.rs |
504 | +++ b/maitred/src/transport.rs |
505 | @@ -115,13 +115,13 @@ impl Decoder for Transport { |
506 | chunk_size, is_last, |
507 | )))); |
508 | self.buf.clear(); |
509 | - Ok(None) |
510 | + Ok(Some(Command(request, None))) |
511 | } |
512 | Request::Data => { |
513 | tracing::info!("Starting data transfer"); |
514 | self.receiver = Some(Box::new(Receiver::Data(DataReceiver::new()))); |
515 | self.buf.clear(); |
516 | - Ok(None) |
517 | + Ok(Some(Command(request, None))) |
518 | } |
519 | _ => Ok(Some(Command(request, None))), |
520 | } |
521 | diff --git a/rfcs/rfc2034.txt b/rfcs/rfc2034.txt |
522 | new file mode 100644 |
523 | index 0000000..14f0ab3 |
524 | --- /dev/null |
525 | +++ b/rfcs/rfc2034.txt |
526 | @@ -0,0 +1,339 @@ |
527 | + |
528 | + |
529 | + |
530 | + |
531 | + |
532 | + |
533 | + Network Working Group N. Freed |
534 | + Request for Comments: RFC 2034 Innosoft |
535 | + Category: Standards Track October 1996 |
536 | + |
537 | + |
538 | + SMTP Service Extension for |
539 | + Returning Enhanced Error Codes |
540 | + |
541 | + Status of this Memo |
542 | + |
543 | + This document specifies an Internet standards track protocol for the |
544 | + Internet community, and requests discussion and suggestions for |
545 | + improvements. Please refer to the current edition of the "Internet |
546 | + Official Protocol Standards" (STD 1) for the standardization state |
547 | + and status of this protocol. Distribution of this memo is unlimited. |
548 | + |
549 | + 1. Abstract |
550 | + |
551 | + This memo defines an extension to the SMTP service [RFC-821, RFC- |
552 | + 1869] whereby an SMTP server augments its responses with the enhanced |
553 | + mail system status codes defined in RFC 1893. These codes can then |
554 | + be used to provide more informative explanations of error conditions, |
555 | + especially in the context of the delivery status notifications format |
556 | + defined in RFC 1894. |
557 | + |
558 | + 2. Introduction |
559 | + |
560 | + Although SMTP is widely and robustly deployed, various extensions |
561 | + have been requested by parts of the Internet community. In |
562 | + particular, in the modern, international, and multilingual Internet a |
563 | + need exists to assign codes to specific error conditions that can be |
564 | + translated into different languages. RFC 1893 defines such a set of |
565 | + status codes and RFC 1894 defines a mechanism to send such coded |
566 | + material to users. However, in many cases the agent creating the RFC |
567 | + 1894 delivery status notification is doing so in response to errors |
568 | + it received from a remote SMTP server. |
569 | + |
570 | + As such, remote servers need a mechanism for embedding enhanced |
571 | + status codes in their responses as well as a way to indicate to a |
572 | + client when they are in fact doing this. This memo uses the SMTP |
573 | + extension mechanism described in RFC 1869 to define such a mechanism. |
574 | + |
575 | + |
576 | + |
577 | + |
578 | + |
579 | + |
580 | + |
581 | + |
582 | + |
583 | + |
584 | + Freed Standards Track [Page 1] |
585 | + |
586 | + RFC 2034 SMTP Enhanced Error Codes October 1996 |
587 | + |
588 | + |
589 | + 3. Framework for the Enhanced Error Statuses Extension |
590 | + |
591 | + The enhanced error statuses transport extension is laid out as |
592 | + follows: |
593 | + |
594 | + (1) the name of the SMTP service extension defined here is |
595 | + Enhanced-Status-Codes; |
596 | + |
597 | + (2) the EHLO keyword value associated with the extension is |
598 | + ENHANCEDSTATUSCODES; |
599 | + |
600 | + (3) no parameter is used with the ENHANCEDSTATUSCODES EHLO |
601 | + keyword; |
602 | + |
603 | + (4) the text part of all 2xx, 4xx, and 5xx SMTP responses |
604 | + other than the initial greeting and any response to |
605 | + HELO or EHLO are prefaced with a status code as defined |
606 | + in RFC 1893. This status code is always followed by one |
607 | + or more spaces. |
608 | + |
609 | + (5) no additional SMTP verbs are defined by this extension; |
610 | + and, |
611 | + |
612 | + (6) the next section specifies how support for the |
613 | + extension affects the behavior of a server and client |
614 | + SMTP. |
615 | + |
616 | + 4. The Enhanced-Status-Codes service extension |
617 | + |
618 | + Servers supporting the Enhanced-Status-Codes extension must preface |
619 | + the text part of almost all response lines with a status code. As in |
620 | + RFC 1893, the syntax of these status codes is given by the ABNF: |
621 | + |
622 | + status-code ::= class "." subject "." detail |
623 | + class ::= "2" / "4" / "5" |
624 | + subject ::= 1*3digit |
625 | + detail ::= 1*3digit |
626 | + |
627 | + These codes must appear in all 2xx, 4xx, and 5xx response lines other |
628 | + than initial greeting and any response to HELO or EHLO. Note that 3xx |
629 | + responses are NOT included in this list. |
630 | + |
631 | + All status codes returned by the server must agree with the primary |
632 | + response code, that is, a 2xx response must incorporate a 2.X.X code, |
633 | + a 4xx response must incorporate a 4.X.X code, and a 5xx response must |
634 | + incorporate a 5.X.X code. |
635 | + |
636 | + |
637 | + |
638 | + |
639 | + |
640 | + Freed Standards Track [Page 2] |
641 | + |
642 | + RFC 2034 SMTP Enhanced Error Codes October 1996 |
643 | + |
644 | + |
645 | + When responses are continued across multiple lines the same status |
646 | + code must appear at the beginning of the text in each line of the |
647 | + response. |
648 | + |
649 | + Servers supporting this extension must attach enhanced status codes |
650 | + to their responses regardless of whether or not EHLO is employed by |
651 | + the client. |
652 | + |
653 | + 5. Status Codes and Negotiation |
654 | + |
655 | + This specification does not provide a means for clients to request |
656 | + that status codes be returned or that they not be returned; a |
657 | + compliant server includes these codes in the responses it sends |
658 | + regardless of whether or not the client expects them. This is |
659 | + somewhat different from most other SMTP extensions, where generally |
660 | + speaking a client must specifically make a request before the |
661 | + extended server behaves any differently than an unextended server. |
662 | + The omission of client negotiation in this case is entirely |
663 | + intentional: Given the generally poor state of SMTP server error code |
664 | + implementation it is felt that any step taken towards more |
665 | + comprehensible error codes is something that all clients, extended or |
666 | + not, should benefit from. |
667 | + |
668 | + IMPORTANT NOTE: The use of this approach in this extension should be |
669 | + seen as a very special case. It MUST NOT be taken as a license for |
670 | + future SMTP extensions to dramatically change the nature of SMTP |
671 | + client-server interaction without proper announcement from the server |
672 | + and a corresponding enabling command from the client. |
673 | + |
674 | + 6. Usage Example |
675 | + |
676 | + The following dialogue illustrates the use of enhanced status codes |
677 | + by a server: |
678 | + |
679 | + S: <wait for connection on TCP port 25> |
680 | + C: <open connection to server> |
681 | + S: 220 dbc.mtview.ca.us SMTP service ready |
682 | + C: EHLO ymir.claremont.edu |
683 | + S: 250-dbc.mtview.ca.us says hello |
684 | + S: 250 ENHANCEDSTATUSCODES |
685 | + C: MAIL FROM:<ned@ymir.claremont.edu> |
686 | + S: 250 2.1.0 Originator <ned@ymir.claremont.edu> ok |
687 | + C: RCPT TO:<mrose@dbc.mtview.ca.us> |
688 | + S: 250 2.1.5 Recipient <mrose@dbc.mtview.ca.us> ok |
689 | + C: RCPT TO:<nosuchuser@dbc.mtview.ca.us> |
690 | + S: 550 5.1.1 Mailbox "nosuchuser" does not exist |
691 | + C: RCPT TO:<remoteuser@isi.edu> |
692 | + S: 551-5.7.1 Forwarding to remote hosts disabled |
693 | + |
694 | + |
695 | + |
696 | + Freed Standards Track [Page 3] |
697 | + |
698 | + RFC 2034 SMTP Enhanced Error Codes October 1996 |
699 | + |
700 | + |
701 | + S: 551 5.7.1 Select another host to act as your forwarder |
702 | + C: DATA |
703 | + S: 354 Send message, ending in CRLF.CRLF. |
704 | + ... |
705 | + C: . |
706 | + S: 250 2.6.0 Message accepted |
707 | + C: QUIT |
708 | + S: 221 2.0.0 Goodbye |
709 | + |
710 | + The client that receives these responses might then send a |
711 | + nondelivery notification of the general form: |
712 | + |
713 | + Date: Mon, 11 Mar 1996 09:21:47 -0400 |
714 | + From: Mail Delivery Subsystem <mailer-daemon@ymir.claremont.edu> |
715 | + Subject: Returned mail |
716 | + To: <ned@ymir.claremont.edu> |
717 | + MIME-Version: 1.0 |
718 | + Content-Type: multipart/report; report-type=delivery-status; |
719 | + boundary="JAA13167.773673707/YMIR.CLAREMONT.EDU" |
720 | + |
721 | + --JAA13167.773673707/YMIR.CLAREMONT.EDU |
722 | + content-type: text/plain; charset=us-ascii |
723 | + |
724 | + ----- Mail was successfully relayed to |
725 | + the following addresses ----- |
726 | + |
727 | + <mrose@dbc.mtview.ca.us> |
728 | + |
729 | + ----- The following addresses had delivery problems ----- |
730 | + <nosuchuser@dbc.mtview.ca.us> |
731 | + (Mailbox "nosuchuser" does not exist) |
732 | + <remoteuser@isi.edu> |
733 | + (Forwarding to remote hosts disabled) |
734 | + |
735 | + --JAA13167.773673707/YMIR.CLAREMONT.EDU |
736 | + content-type: message/delivery-status |
737 | + |
738 | + Reporting-MTA: dns; ymir.claremont.edu |
739 | + |
740 | + Original-Recipient: rfc822;mrose@dbc.mtview.ca.us |
741 | + Final-Recipient: rfc822;mrose@dbc.mtview.ca.us |
742 | + Action: relayed |
743 | + Status: 2.1.5 (Destination address valid) |
744 | + Diagnostic-Code: smtp; |
745 | + 250 Recipient <mrose@dbc.mtview.ca.us> ok |
746 | + Remote-MTA: dns; dbc.mtview.ca.us |
747 | + |
748 | + |
749 | + |
750 | + |
751 | + |
752 | + Freed Standards Track [Page 4] |
753 | + |
754 | + RFC 2034 SMTP Enhanced Error Codes October 1996 |
755 | + |
756 | + |
757 | + Original-Recipient: rfc822;nosuchuser@dbc.mtview.ca.us |
758 | + Final-Recipient: rfc822;nosuchuser@dbc.mtview.ca.us |
759 | + Action: failed |
760 | + Status: 5.1.1 (Bad destination mailbox address) |
761 | + Diagnostic-Code: smtp; |
762 | + 550 Mailbox "nosuchuser" does not exist |
763 | + Remote-MTA: dns; dbc.mtview.ca.us |
764 | + |
765 | + Original-Recipient: rfc822;remoteuser@isi.edu |
766 | + Final-Recipient: rfc822;remoteuser@isi.edu |
767 | + Action: failed |
768 | + Status: 5.7.1 (Delivery not authorized, message refused) |
769 | + Diagnostic-Code: smtp; |
770 | + 551 Forwarding to remote hosts disabled |
771 | + Select another host to act as your forwarder |
772 | + Remote-MTA: dns; dbc.mtview.ca.us |
773 | + |
774 | + --JAA13167.773673707/YMIR.CLAREMONT.EDU |
775 | + content-type: message/rfc822 |
776 | + |
777 | + [original message goes here] |
778 | + --JAA13167.773673707/YMIR.CLAREMONT.EDU-- |
779 | + |
780 | + Note that in order to reduce clutter the reporting MTA has omitted |
781 | + enhanced status code information from the diagnostic-code fields it |
782 | + has generated. |
783 | + |
784 | + 7. Security Considerations |
785 | + |
786 | + Additional detail in server responses axiomatically provides |
787 | + additional information about the server. It is conceivable that |
788 | + additional information of this sort may be of assistance in |
789 | + circumventing server security. The advantages of provides additional |
790 | + information must always be weighed against the security implications |
791 | + of doing so. |
792 | + |
793 | + |
794 | + |
795 | + |
796 | + |
797 | + |
798 | + |
799 | + |
800 | + |
801 | + |
802 | + |
803 | + |
804 | + |
805 | + |
806 | + |
807 | + |
808 | + Freed Standards Track [Page 5] |
809 | + |
810 | + RFC 2034 SMTP Enhanced Error Codes October 1996 |
811 | + |
812 | + |
813 | + 8. References |
814 | + |
815 | + [RFC-821] |
816 | + Postel, J., "Simple Mail Transfer Protocol", RFC 821, |
817 | + August, 1982. (August, 1982). |
818 | + |
819 | + [RFC-1869] |
820 | + Rose, M., Stefferud, E., Crocker, C., Klensin, J., Freed, |
821 | + N., "SMTP Service Extensions", RFC 1869, November, 1995. |
822 | + |
823 | + [RFC-1893] |
824 | + Vaudreuil, G., "Enhanced Mail System Status Codes", RFC |
825 | + 1893, January, 1996. |
826 | + |
827 | + [RFC-1894] |
828 | + Moore, K., Vaudreuil, G., "An Extensible Message Format |
829 | + for Delivery Status Notifications", RFC 1894, January, |
830 | + 1996. |
831 | + |
832 | + 9. Author Address |
833 | + |
834 | + Ned Freed |
835 | + Innosoft International, Inc. |
836 | + 1050 East Garvey Avenue South |
837 | + West Covina, CA 91790 |
838 | + USA |
839 | + tel: +1 818 919 3600 fax: +1 818 919 3614 |
840 | + email: ned@innosoft.com |
841 | + |
842 | + |
843 | + |
844 | + |
845 | + |
846 | + |
847 | + |
848 | + |
849 | + |
850 | + |
851 | + |
852 | + |
853 | + |
854 | + |
855 | + |
856 | + |
857 | + |
858 | + |
859 | + |
860 | + |
861 | + |
862 | + |
863 | + |
864 | + Freed Standards Track [Page 6] |
865 | + |
866 | diff --git a/scripts/swaks_test.sh b/scripts/swaks_test.sh |
867 | new file mode 100755 |
868 | index 0000000..20f2da4 |
869 | --- /dev/null |
870 | +++ b/scripts/swaks_test.sh |
871 | @@ -0,0 +1,6 @@ |
872 | + #!/bin/sh |
873 | + |
874 | + # Uses swaks: https://www.jetmore.org/john/code/swaks/ to do some basic SMTP |
875 | + # verification. Make sure you install the tool first! |
876 | + |
877 | + swaks --to hello@example.com --server localhost:2525 |