Author:
Hash:
Timestamp:
+679 -39 +/-7 browse
Kevin Schoon [me@kevinschoon.com]
8acf1fe8340a1358206bf0b7bf42571dfd4112c1
Mon, 29 Jul 2024 17:33:46 +0000 (1.3 years ago)
| 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 |