Commit
Author: Kevin Schoon [me@kevinschoon.com]
Hash: 8acf1fe8340a1358206bf0b7bf42571dfd4112c1
Timestamp: Mon, 29 Jul 2024 17:33:46 +0000 (4 months ago)

+679 -39 +/-7 browse
write several tests, various refactoring
1diff --git a/cmd/maitred-debug/src/main.rs b/cmd/maitred-debug/src/main.rs
2index 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
21index 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
31index 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
134index 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
502index 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
522new file mode 100644
523index 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
867new file mode 100755
868index 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