Author: Kevin Schoon [me@kevinschoon.com]
Hash: 3a444a06440a4c1380f9d212fa5a3657a127fb66
Timestamp: Mon, 12 Aug 2024 17:45:46 +0000 (2 months ago)

+163 -119 +/-4 browse
make session return command chunks
1diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs
2index e523b2c..7ea9531 100644
3--- a/maitred/src/lib.rs
4+++ b/maitred/src/lib.rs
5 @@ -7,46 +7,79 @@ mod session;
6 mod transport;
7 mod verify;
8
9- use smtp_proto::{Request, Response as SmtpResponse};
10+ use smtp_proto::Request;
11
12 /// Low Level SMTP protocol is exported for convenience
13 pub use smtp_proto;
14
15 pub use error::Error;
16 pub use server::Server;
17+ use transport::Response;
18
19- /// Generate an SMTP response
20- #[macro_export(local_inner_macros)]
21+ /// Chunk is a logical set of SMTP resposnes that might be generated from one
22+ /// command or "pipelined" as the result of several commands sent by the client
23+ /// that do not require immediate responses.
24+ #[derive(Clone, Debug)]
25+ pub(crate) struct Chunk(pub Vec<Response<String>>);
26+
27+ impl Chunk {
28+ pub fn new() -> Self {
29+ Chunk(vec![])
30+ }
31+ }
32+
33+ impl PartialEq for Chunk {
34+ fn eq(&self, other: &Self) -> bool {
35+ self.0.len() == other.0.len() && self.0.iter().zip(other.0.iter()).all(|(a, b)| a == b)
36+ }
37+ }
38+
39+ /// Generate a single smtp_response
40 macro_rules! smtp_response {
41 ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => {
42 Response::General(SmtpResponse::new($code, $e1, $e2, $e3, $name.to_string()))
43 };
44 }
45+ pub(crate) use smtp_response;
46
47- /// Generate a successful SMTP response
48- #[macro_export(local_inner_macros)]
49- macro_rules! smtp_ok {
50+ /// Generate an SMTP response
51+ macro_rules! smtp_chunk {
52+ ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => {
53+ Chunk(vec![Response::General(SmtpResponse::new(
54+ $code,
55+ $e1,
56+ $e2,
57+ $e3,
58+ $name.to_string(),
59+ ))])
60+ };
61+ }
62+ pub(crate) use smtp_chunk;
63+
64+ /// Generate an SMTP response error
65+ macro_rules! smtp_chunk_ok {
66 ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => {
67- Ok::<Response<String>, Response<String>>(Response::General(SmtpResponse::new(
68+ Ok::<Chunk, Chunk>(Chunk(vec![Response::General(SmtpResponse::new(
69 $code,
70 $e1,
71 $e2,
72 $e3,
73 $name.to_string(),
74- )))
75+ ))]))
76 };
77 }
78+ pub(crate) use smtp_chunk_ok;
79
80 /// Generate an SMTP response error
81- #[macro_export(local_inner_macros)]
82- macro_rules! smtp_err {
83+ macro_rules! smtp_chunk_err {
84 ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => {
85- Err::<Response<String>, Response<String>>(Response::General(SmtpResponse::new(
86+ Err::<Chunk, Chunk>(Chunk(vec![Response::General(SmtpResponse::new(
87 $code,
88 $e1,
89 $e2,
90 $e3,
91 $name.to_string(),
92- )))
93+ ))]))
94 };
95 }
96+ pub(crate) use smtp_chunk_err;
97 diff --git a/maitred/src/pipeline.rs b/maitred/src/pipeline.rs
98index 750f90d..cc99703 100644
99--- a/maitred/src/pipeline.rs
100+++ b/maitred/src/pipeline.rs
101 @@ -1,10 +1,22 @@
102 use crate::session::Result as SessionResult;
103- use crate::transport::Response;
104- use crate::Request;
105+ use crate::{Chunk, Request};
106
107- pub type Result = Vec<Response<String>>;
108 pub type Transaction = (Request<String>, SessionResult);
109
110+ fn flatten(history: &[Transaction]) -> Chunk {
111+ Chunk(
112+ history
113+ .iter()
114+ .map(|tx| tx.1.clone().unwrap_or_else(|e| e))
115+ .fold(Vec::new(), |mut accm, chunk| {
116+ accm.extend(chunk.0.clone());
117+ accm
118+ })
119+ .into_iter()
120+ .collect(),
121+ )
122+ }
123+
124 /// Pipeline chunks session request/responses into logical groups returning
125 /// a response only once a session is considered "completed".
126 #[derive(Default)]
127 @@ -31,7 +43,7 @@ impl Pipeline {
128 .is_some_and(|is_tx| is_tx)
129 }
130
131- fn chunk(&mut self) -> Result {
132+ fn chunk(&mut self) -> Chunk {
133 let mail_from_ok = self.history.iter().fold(false, |ok, tx| {
134 if matches!(&tx.0, Request::Mail { from: _ }) {
135 return tx.1.is_ok();
136 @@ -49,27 +61,15 @@ impl Pipeline {
137 .last()
138 .expect("to results called without history");
139 if last_command.1.is_ok() && mail_from_ok && rcpt_to_ok_count > 0 {
140- self.history
141- .iter()
142- .map(|tx| tx.1.clone().unwrap_or_else(|e| e))
143- .collect()
144+ flatten(&self.history)
145 } else if !mail_from_ok {
146 self.history.pop();
147- self.history
148- .iter()
149- .map(|tx| tx.1.clone().unwrap_or_else(|e| e))
150- .collect()
151+ flatten(&self.history)
152 } else if !rcpt_to_ok_count <= 0 {
153 self.history.pop();
154- self.history
155- .iter()
156- .map(|tx| tx.1.clone().unwrap_or_else(|e| e))
157- .collect()
158+ flatten(&self.history)
159 } else {
160- self.history
161- .iter()
162- .map(|tx| tx.1.clone().unwrap_or_else(|e| e))
163- .collect()
164+ flatten(&self.history)
165 }
166 }
167
168 @@ -89,7 +89,7 @@ impl Pipeline {
169 /// results if the command requires a server response. Returns true if
170 /// the process was successful or false if it contained fatal errors. All
171 /// results must be passed to the client.
172- pub fn process(&mut self, req: &Request<String>, res: &SessionResult) -> Result {
173+ pub fn process(&mut self, req: &Request<String>, res: &SessionResult) -> Chunk {
174 let is_data_tx = self.within_tx();
175 if is_data_tx {
176 // ignore the first data request
177 @@ -101,18 +101,18 @@ impl Pipeline {
178 match req {
179 Request::Ehlo { host: _ } => {
180 self.history.clear();
181- vec![res.clone().unwrap_or_else(|e| e)]
182+ res.clone().unwrap_or_else(|e| e)
183 }
184 Request::Lhlo { host: _ } => {
185 self.history.clear();
186- vec![res.clone().unwrap_or_else(|e| e)]
187+ res.clone().unwrap_or_else(|e| e)
188 }
189 Request::Helo { host: _ } => {
190 self.history.clear();
191- vec![res.clone().unwrap_or_else(|e| e)]
192+ res.clone().unwrap_or_else(|e| e)
193 }
194- Request::Mail { from: _ } => vec![],
195- Request::Rcpt { to: _ } => vec![],
196+ Request::Mail { from: _ } => Chunk::new(),
197+ Request::Rcpt { to: _ } => Chunk::new(),
198 Request::Bdat {
199 chunk_size: _,
200 is_last: _,
201 @@ -122,17 +122,17 @@ impl Pipeline {
202 self.history.clear();
203 chunk
204 } else {
205- vec![]
206+ Chunk::new()
207 }
208 }
209 Request::Auth {
210 mechanism: _,
211 initial_response: _,
212 } => todo!(),
213- Request::Noop { value: _ } => vec![res.clone().unwrap_or_else(|e| e)],
214+ Request::Noop { value: _ } => res.clone().unwrap_or_else(|e| e),
215 Request::Vrfy { value: _ } => todo!(),
216 Request::Expn { value: _ } => todo!(),
217- Request::Help { value: _ } => vec![res.clone().unwrap_or_else(|e| e)],
218+ Request::Help { value: _ } => res.clone().unwrap_or_else(|e| e),
219 Request::Etrn { name: _ } => todo!(),
220 Request::Atrn { domains: _ } => todo!(),
221 Request::Burl { uri: _, is_last: _ } => todo!(),
222 @@ -143,14 +143,14 @@ impl Pipeline {
223 self.history.clear();
224 chunk
225 } else {
226- vec![]
227+ Chunk::new()
228 }
229 }
230 Request::Rset => {
231 self.history.clear();
232- vec![res.clone().unwrap_or_else(|e| e)]
233+ res.clone().unwrap_or_else(|e| e)
234 }
235- Request::Quit => vec![res.clone().unwrap_or_else(|e| e)],
236+ Request::Quit => res.clone().unwrap_or_else(|e| e),
237 }
238 }
239 }
240 @@ -159,7 +159,9 @@ impl Pipeline {
241 mod test {
242
243 use super::*;
244- use crate::{smtp_ok, Request, SmtpResponse};
245+ use crate::{
246+ smtp_chunk_ok, smtp_proto::Response as SmtpResponse, transport::Response, Request,
247+ };
248
249 #[test]
250 pub fn test_pipeline_basic() {
251 @@ -170,8 +172,9 @@ mod test {
252 &Request::Helo {
253 host: "example.org".to_string(),
254 },
255- &smtp_ok!(200, 0, 0, 0, "OK")
256+ &smtp_chunk_ok!(200, 0, 0, 0, "OK")
257 )
258+ .0
259 .len()
260 == 1
261 );
262 @@ -184,8 +187,9 @@ mod test {
263 ..Default::default()
264 },
265 },
266- &smtp_ok!(200, 0, 0, 0, "OK: baz@qux.com")
267+ &smtp_chunk_ok!(200, 0, 0, 0, "OK: baz@qux.com")
268 )
269+ .0
270 .is_empty());
271 assert!(pipeline
272 .process(
273 @@ -195,16 +199,18 @@ mod test {
274 ..Default::default()
275 }
276 },
277- &smtp_ok!(200, 0, 0, 0, "OK: fuu@bar.com")
278+ &smtp_chunk_ok!(200, 0, 0, 0, "OK: fuu@bar.com")
279 )
280+ .0
281 .is_empty());
282
283 // initialize a data request
284 assert!(pipeline
285- .process(&Request::Data {}, &smtp_ok!(200, 0, 0, 0, "OK"))
286+ .process(&Request::Data {}, &smtp_chunk_ok!(200, 0, 0, 0, "OK"))
287+ .0
288 .is_empty());
289 // simulate the end of a request
290- let result = pipeline.process(&Request::Data {}, &smtp_ok!(200, 0, 0, 0, "OK"));
291- assert!(result.len() == 3);
292+ let result = pipeline.process(&Request::Data {}, &smtp_chunk_ok!(200, 0, 0, 0, "OK"));
293+ assert!(result.0.len() == 3);
294 }
295 }
296 diff --git a/maitred/src/server.rs b/maitred/src/server.rs
297index 4d1fb2e..507c947 100644
298--- a/maitred/src/server.rs
299+++ b/maitred/src/server.rs
300 @@ -4,13 +4,14 @@ use bytes::Bytes;
301 use futures::SinkExt;
302 use smtp_proto::Request;
303 use tokio::{net::TcpListener, time::timeout};
304- use tokio_stream::{self as stream, StreamExt};
305+ use tokio_stream::StreamExt;
306 use tokio_util::codec::Framed;
307
308 use crate::error::Error;
309 use crate::pipeline::Pipeline;
310 use crate::session::Session;
311- use crate::transport::{Response, Transport};
312+ use crate::transport::Transport;
313+ use crate::Chunk;
314
315 const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:2525";
316 const DEFAULT_GREETING: &str = "Maitred ESMTP Server";
317 @@ -48,7 +49,7 @@ struct ConditionalPipeline<'a> {
318 }
319
320 impl ConditionalPipeline<'_> {
321- pub fn apply(&mut self, req: &Request<String>, data: Option<&Bytes>) -> Vec<Response<String>> {
322+ pub fn apply(&mut self, req: &Request<String>, data: Option<&Bytes>) -> Chunk {
323 let response = self.session.process(req, data);
324 if self.session.has_capability(smtp_proto::EXT_PIPELINING) && self.session.is_extended() {
325 self.pipeline.process(req, &response)
326 @@ -56,11 +57,11 @@ impl ConditionalPipeline<'_> {
327 match response {
328 Ok(response) => {
329 tracing::debug!("Client response: {:?}", response);
330- vec![response]
331+ response
332 }
333 Err(response) => {
334 tracing::warn!("Client error: {:?}", response);
335- vec![response]
336+ response
337 }
338 }
339 }
340 @@ -157,7 +158,7 @@ impl Server {
341 finished = true;
342 }
343 let responses = pipelined.apply(&command.0, command.1.as_ref());
344- for response in responses {
345+ for response in responses.0.into_iter() {
346 framed.send(response).await?;
347 }
348 if finished {
349 diff --git a/maitred/src/session.rs b/maitred/src/session.rs
350index dcba00d..d1598b7 100644
351--- a/maitred/src/session.rs
352+++ b/maitred/src/session.rs
353 @@ -10,11 +10,12 @@ use url::Host;
354 use crate::expansion::Expansion;
355 use crate::transport::Response;
356 use crate::verify::Verify;
357- use crate::{smtp_err, smtp_ok, smtp_response};
358+ use crate::{smtp_chunk, smtp_chunk_err, smtp_chunk_ok};
359+ use crate::{smtp_response, Chunk};
360
361 /// Result generated as part of an SMTP session, an Err indicates a session
362 /// level error that will be returned to the client.
363- pub type Result = StdResult<Response<String>, Response<String>>;
364+ pub type Result = StdResult<Chunk, Chunk>;
365
366 enum Mode {
367 Legacy,
368 @@ -119,23 +120,17 @@ impl Session {
369 }
370
371 /// ensure that the session has been initialized otherwise return an error
372- fn check_initialized(&self) -> StdResult<(), Response<String>> {
373+ fn check_initialized(&self) -> StdResult<(), Chunk> {
374 if self.initialized.is_none() {
375- return Err(smtp_response!(
376- 500,
377- 0,
378- 0,
379- 0,
380- "It's polite to say EHLO first"
381- ));
382+ return Err(smtp_chunk!(500, 0, 0, 0, "It's polite to say EHLO first"));
383 }
384 Ok(())
385 }
386
387 /// checks if 8BITMIME is supported
388- fn check_body(&self, body: &[u8]) -> StdResult<(), Response<String>> {
389+ fn check_body(&self, body: &[u8]) -> StdResult<(), Chunk> {
390 if !self.has_capability(smtp_proto::EXT_8BIT_MIME) && !body.is_ascii() {
391- return Err(smtp_response!(
392+ return Err(smtp_chunk!(
393 500,
394 0,
395 0,
396 @@ -157,35 +152,32 @@ impl Session {
397 pub fn process(&mut self, req: &Request<String>, data: Option<&Bytes>) -> Result {
398 match req {
399 Request::Ehlo { host } => {
400- self.hostname = Some(Host::parse(host).map_err(|e| {
401- Response::General(SmtpResponse::new(500, 0, 0, 0, e.to_string()))
402- })?);
403+ self.hostname =
404+ Some(Host::parse(host).map_err(|e| smtp_chunk!(500, 0, 0, 0, e.to_string()))?);
405 self.reset();
406 self.initialized = Some(Mode::Extended);
407 let mut resp = EhloResponse::new(format!("Hello {}", host));
408 resp.capabilities = self.capabilities;
409 resp.size = self.maximum_size as usize;
410- Ok(Response::Ehlo(resp))
411+ Ok(Chunk(vec![Response::Ehlo(resp)]))
412 }
413 Request::Lhlo { host } => {
414- self.hostname =
415- Some(Host::parse(host).map_err(|e| smtp_response!(500, 0, 0, 0, e))?);
416+ self.hostname = Some(Host::parse(host).map_err(|e| smtp_chunk!(500, 0, 0, 0, e))?);
417 self.reset();
418 self.initialized = Some(Mode::Legacy);
419- smtp_ok!(250, 0, 0, 0, format!("Hello {}", host))
420+ smtp_chunk_ok!(250, 0, 0, 0, format!("Hello {}", host))
421 }
422 Request::Helo { host } => {
423- self.hostname = Some(
424- Host::parse(host).map_err(|e| smtp_response!(500, 0, 0, 0, e.to_string()))?,
425- );
426+ self.hostname =
427+ Some(Host::parse(host).map_err(|e| smtp_chunk!(500, 0, 0, 0, e.to_string()))?);
428 self.reset();
429 self.initialized = Some(Mode::Legacy);
430- smtp_ok!(250, 0, 0, 0, format!("Hello {}", host))
431+ smtp_chunk_ok!(250, 0, 0, 0, format!("Hello {}", host))
432 }
433 Request::Mail { from } => {
434 self.check_initialized()?;
435 let mail_from = EmailAddress::from_str(from.address.as_str()).map_err(|e| {
436- smtp_response!(
437+ smtp_chunk!(
438 500,
439 0,
440 0,
441 @@ -194,19 +186,19 @@ impl Session {
442 )
443 })?;
444 self.mail_from = Some(mail_from.clone());
445- smtp_ok!(250, 0, 0, 0, "OK")
446+ smtp_chunk_ok!(250, 0, 0, 0, "OK")
447 }
448 Request::Rcpt { to } => {
449 self.check_initialized()?;
450 let rcpt_to = EmailAddress::from_str(to.address.as_str()).map_err(|e| {
451- smtp_response!(500, 0, 0, 0, format!("cannot parse: {} {}", to.address, e))
452+ smtp_chunk!(500, 0, 0, 0, format!("cannot parse: {} {}", to.address, e))
453 })?;
454 if let Some(ref mut rcpts) = self.rcpt_to {
455 rcpts.push(rcpt_to.clone());
456 } else {
457 self.rcpt_to = Some(vec![rcpt_to.clone()]);
458 }
459- smtp_ok!(250, 0, 0, 0, "OK")
460+ smtp_chunk_ok!(250, 0, 0, 0, "OK")
461 }
462 Request::Bdat {
463 chunk_size: _,
464 @@ -226,8 +218,8 @@ impl Session {
465 self.check_body(&message_payload)?;
466 let parser = MessageParser::new();
467 let response = match parser.parse(&message_payload) {
468- Some(_) => smtp_ok!(250, 0, 0, 0, "OK"),
469- None => smtp_err!(
470+ Some(_) => smtp_chunk_ok!(250, 0, 0, 0, "OK"),
471+ None => smtp_chunk_err!(
472 500,
473 0,
474 0,
475 @@ -243,7 +235,7 @@ impl Session {
476 } else {
477 tracing::info!("Initializing data transfer mode");
478 self.data_transfer = Some(DataTransfer::Bdat);
479- smtp_ok!(354, 0, 0, 0, "Starting BDAT data transfer".to_string())
480+ smtp_chunk_ok!(354, 0, 0, 0, "Starting BDAT data transfer".to_string())
481 }
482 }
483 Request::Auth {
484 @@ -252,42 +244,42 @@ impl Session {
485 } => todo!(),
486 Request::Noop { value: _ } => {
487 self.check_initialized()?;
488- smtp_ok!(250, 0, 0, 0, "OK".to_string())
489+ smtp_chunk_ok!(250, 0, 0, 0, "OK".to_string())
490 }
491 Request::Vrfy { value } => {
492 if let Some(verifier) = &self.verification {
493 let address = EmailAddress::from_str(value.as_str()).map_err(|e| {
494- smtp_response!(500, 0, 0, 0, format!("cannot parse: {} {}", value, e))
495+ smtp_chunk!(500, 0, 0, 0, format!("cannot parse: {} {}", value, e))
496 })?;
497 match verifier.verify(&address) {
498 Ok(_) => {
499- smtp_ok!(200, 0, 0, 0, "Ok".to_string())
500+ smtp_chunk_ok!(200, 0, 0, 0, "Ok".to_string())
501 }
502 Err(e) => match e {
503 crate::verify::Error::Server(e) => {
504- smtp_err!(500, 0, 0, 0, e.to_string())
505+ smtp_chunk_err!(500, 0, 0, 0, e.to_string())
506 }
507 crate::verify::Error::NotFound(e) => {
508- smtp_err!(500, 0, 0, 0, e.to_string())
509+ smtp_chunk_err!(500, 0, 0, 0, e.to_string())
510 }
511 crate::verify::Error::Ambiguous(alternatives) => {
512- smtp_err!(500, 0, 0, 0, alternatives.to_string())
513+ smtp_chunk_err!(500, 0, 0, 0, alternatives.to_string())
514 }
515 },
516 }
517 } else {
518- smtp_err!(500, 0, 0, 0, "No such address")
519+ smtp_chunk_err!(500, 0, 0, 0, "No such address")
520 }
521 }
522 Request::Expn { value } => {
523 if let Some(expn) = &self.list_expansion {
524 match expn.expand(value) {
525 Ok(addresses) => {
526- smtp_ok!(250, 0, 0, 0, addresses.to_string())
527+ smtp_chunk_ok!(250, 0, 0, 0, addresses.to_string())
528 }
529 Err(err) => match err {
530 crate::expansion::Error::Server(message) => {
531- smtp_err!(
532+ smtp_chunk_err!(
533 500,
534 0,
535 0,
536 @@ -296,20 +288,26 @@ impl Session {
537 )
538 }
539 crate::expansion::Error::NotFound(name) => {
540- smtp_err!(500, 0, 0, 0, format!("No such mailing list: {}", name))
541+ smtp_chunk_err!(
542+ 500,
543+ 0,
544+ 0,
545+ 0,
546+ format!("No such mailing list: {}", name)
547+ )
548 }
549 },
550 }
551 } else {
552- smtp_err!(500, 0, 0, 0, "Server does not support EXPN")
553+ smtp_chunk_err!(500, 0, 0, 0, "Server does not support EXPN")
554 }
555 }
556 Request::Help { value } => {
557 self.check_initialized()?;
558 if value.is_empty() {
559- smtp_ok!(250, 0, 0, 0, self.help_banner.to_string())
560+ smtp_chunk_ok!(250, 0, 0, 0, self.help_banner.to_string())
561 } else {
562- smtp_ok!(
563+ smtp_chunk_ok!(
564 250,
565 0,
566 0,
567 @@ -337,8 +335,8 @@ impl Session {
568 self.check_body(&message_payload)?;
569 let parser = MessageParser::new();
570 let response = match parser.parse(&message_payload) {
571- Some(_) => smtp_ok!(250, 0, 0, 0, "OK".to_string()),
572- None => smtp_err!(
573+ Some(_) => smtp_chunk_ok!(250, 0, 0, 0, "OK".to_string()),
574+ None => smtp_chunk_err!(
575 500,
576 0,
577 0,
578 @@ -354,7 +352,7 @@ impl Session {
579 } else {
580 tracing::info!("Initializing data transfer mode");
581 self.data_transfer = Some(DataTransfer::Data);
582- smtp_ok!(
583+ smtp_chunk_ok!(
584 354,
585 0,
586 0,
587 @@ -366,9 +364,9 @@ impl Session {
588 Request::Rset => {
589 self.check_initialized()?;
590 self.reset();
591- smtp_ok!(200, 0, 0, 0, "".to_string())
592+ smtp_chunk_ok!(200, 0, 0, 0, "".to_string())
593 }
594- Request::Quit => smtp_ok!(221, 0, 0, 0, "Ciao!".to_string()),
595+ Request::Quit => smtp_chunk_ok!(221, 0, 0, 0, "Ciao!".to_string()),
596 }
597 }
598 }
599 @@ -439,12 +437,12 @@ mod test {
600 host: EXAMPLE_HOSTNAME.to_string(),
601 },
602 payload: None,
603- expected: smtp_ok!(250, 0, 0, 0, String::from("Hello example.org")),
604+ expected: smtp_chunk_ok!(250, 0, 0, 0, String::from("Hello example.org")),
605 },
606 TestCase {
607 request: Request::Quit {},
608 payload: None,
609- expected: smtp_ok!(221, 0, 0, 0, String::from("Ciao!")),
610+ expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")),
611 },
612 ];
613 let mut session = Session::default();
614 @@ -465,7 +463,7 @@ mod test {
615 },
616 },
617 payload: None,
618- expected: smtp_err!(500, 0, 0, 0, String::from("It's polite to say EHLO first")),
619+ expected: smtp_chunk_err!(500, 0, 0, 0, String::from("It's polite to say EHLO first")),
620 }];
621 let mut session = Session::default().our_hostname(EXAMPLE_HOSTNAME);
622 process_all(&mut session, requests);
623 @@ -481,12 +479,12 @@ mod test {
624 host: EXAMPLE_HOSTNAME.to_string(),
625 },
626 payload: None,
627- expected: smtp_ok!(250, 0, 0, 0, String::from("Hello example.org")),
628+ expected: smtp_chunk_ok!(250, 0, 0, 0, String::from("Hello example.org")),
629 },
630 TestCase {
631 request: Request::Data {},
632 payload: None,
633- expected: smtp_ok!(
634+ expected: smtp_chunk_ok!(
635 354,
636 0,
637 0,
638 @@ -502,7 +500,13 @@ mod test {
639 "#
640 .as_bytes(),
641 )),
642- expected: smtp_err!(500, 0, 0, 0, "Non ascii characters found in message body"),
643+ expected: smtp_chunk_err!(
644+ 500,
645+ 0,
646+ 0,
647+ 0,
648+ "Non ascii characters found in message body"
649+ ),
650 },
651 // upgrade the connection to extended mode
652 TestCase {
653 @@ -510,12 +514,12 @@ mod test {
654 host: EXAMPLE_HOSTNAME.to_string(),
655 },
656 payload: None,
657- expected: Ok(Response::Ehlo(expected_ehlo_response)),
658+ expected: Ok(Chunk(vec![Response::Ehlo(expected_ehlo_response)])),
659 },
660 TestCase {
661 request: Request::Data {},
662 payload: None,
663- expected: smtp_ok!(
664+ expected: smtp_chunk_ok!(
665 354,
666 0,
667 0,
668 @@ -531,7 +535,7 @@ mod test {
669 "#
670 .as_bytes(),
671 )),
672- expected: smtp_ok!(250, 0, 0, 0, "OK"),
673+ expected: smtp_chunk_ok!(250, 0, 0, 0, "OK"),
674 },
675 ];
676 let mut session = Session::default()
677 @@ -548,7 +552,7 @@ mod test {
678 host: EXAMPLE_HOSTNAME.to_string(),
679 },
680 payload: None,
681- expected: smtp_ok!(250, 0, 0, 0, "Hello example.org"),
682+ expected: smtp_chunk_ok!(250, 0, 0, 0, "Hello example.org"),
683 },
684 TestCase {
685 request: Request::Mail {
686 @@ -558,7 +562,7 @@ mod test {
687 },
688 },
689 payload: None,
690- expected: smtp_ok!(250, 0, 0, 0, "OK"),
691+ expected: smtp_chunk_ok!(250, 0, 0, 0, "OK"),
692 },
693 TestCase {
694 request: Request::Rcpt {
695 @@ -568,13 +572,13 @@ mod test {
696 },
697 },
698 payload: None,
699- expected: smtp_ok!(250, 0, 0, 0, "OK"),
700+ expected: smtp_chunk_ok!(250, 0, 0, 0, "OK"),
701 },
702 // initiate data transfer
703 TestCase {
704 request: Request::Data {},
705 payload: None,
706- expected: smtp_ok!(
707+ expected: smtp_chunk_ok!(
708 354,
709 0,
710 0,
711 @@ -594,7 +598,7 @@ Note that it doesn't end with a "." since that parsing happens as part of the
712 transport rather than the session.
713 "#,
714 )),
715- expected: smtp_ok!(250, 0, 0, 0, "OK"),
716+ expected: smtp_chunk_ok!(250, 0, 0, 0, "OK"),
717 },
718 ];
719 let mut session = Session::default().our_hostname(EXAMPLE_HOSTNAME);