Skip to main content

smtp_proto/response/
parser.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use std::slice::Iter;
8
9use crate::{request::parser::Rfc5321Parser, *};
10
11use super::*;
12
13pub const MAX_RESPONSE_LENGTH: usize = 4096;
14
15#[derive(Default)]
16pub struct ResponseReceiver {
17    buf: Vec<u8>,
18    code: u16,
19    esc: [u8; 3],
20    esc_cur: [u8; 3],
21    esc_restore: [u8; 10],
22    esc_pos: usize,
23    is_last: bool,
24    pos: usize,
25}
26
27impl ResponseReceiver {
28    pub fn from_code(code: u16) -> Self {
29        Self {
30            code,
31            esc: [0u8; 3],
32            esc_cur: [0u8; 3],
33            esc_restore: [0u8; 10],
34            esc_pos: 0,
35            pos: 3,
36            is_last: false,
37            buf: Vec::new(),
38        }
39    }
40
41    pub fn parse(&mut self, bytes: &mut Iter<'_, u8>) -> Result<Response<String>, Error> {
42        for &ch in bytes {
43            match self.pos {
44                0..=2 => {
45                    if ch.is_ascii_digit() {
46                        if self.buf.is_empty() {
47                            self.code = self
48                                .code
49                                .saturating_mul(10)
50                                .saturating_add((ch - b'0') as u16);
51                        }
52                        self.pos += 1;
53                    } else {
54                        return Err(Error::SyntaxError {
55                            syntax: "Invalid response code",
56                        });
57                    }
58                }
59                3 => match ch {
60                    b' ' => {
61                        self.is_last = true;
62                        self.pos += 1;
63                    }
64                    b'-' => {
65                        self.pos += 1;
66                    }
67                    b'\r' => {
68                        continue;
69                    }
70                    b'\n' => {
71                        self.is_last = true;
72                    }
73                    _ => {
74                        return Err(Error::SyntaxError {
75                            syntax: "Invalid response separator",
76                        });
77                    }
78                },
79                4..=6 => {
80                    let mut do_restore = false;
81
82                    match ch {
83                        b'0'..=b'9' => {
84                            let code = &mut self.esc_cur[self.pos - 4];
85                            if *code < 100 {
86                                if let Some(restore) = self.esc_restore.get_mut(self.esc_pos) {
87                                    *restore = ch;
88                                    *code = code.saturating_mul(10).saturating_add(ch - b'0');
89                                    self.esc_pos += 1;
90                                } else {
91                                    do_restore = true;
92                                }
93                            } else {
94                                do_restore = true;
95                            }
96                        }
97                        b'.' if self.pos < 6 => {
98                            if let Some(restore) = self.esc_restore.get_mut(self.esc_pos) {
99                                *restore = ch;
100                                self.pos += 1;
101                                self.esc_pos += 1;
102                            } else {
103                                do_restore = true;
104                            }
105                        }
106                        b' ' | b'\r' | b'\n' if self.pos == 6 => {
107                            self.esc = self.esc_cur;
108                            self.esc_cur.fill(0);
109                            self.pos = 7;
110                            self.esc_pos = 0;
111                        }
112                        _ => {
113                            do_restore = true;
114                        }
115                    }
116                    if do_restore {
117                        // ESC parsing failed, restore parsed digits
118                        if self.esc_pos > 0 {
119                            self.buf
120                                .extend_from_slice(&self.esc_restore[..self.esc_pos]);
121                        }
122                        self.buf.push(ch);
123                        self.pos = 7;
124                        self.esc_pos = 0;
125                        self.esc_cur.fill(0);
126                    }
127                }
128                _ => match ch {
129                    b'\r' | b'\n' => (),
130                    _ => {
131                        if self.buf.len() < MAX_RESPONSE_LENGTH {
132                            self.buf.push(ch);
133                        } else {
134                            return Err(Error::ResponseTooLong);
135                        }
136                    }
137                },
138            }
139
140            if ch == b'\n' {
141                if self.is_last {
142                    return Ok(Response {
143                        code: self.code,
144                        esc: self.esc,
145                        message: std::mem::take(&mut self.buf).into_string(),
146                    });
147                } else {
148                    self.buf.push(b'\n');
149                    self.pos = 0;
150                }
151            }
152        }
153
154        Err(Error::NeedsMoreData { bytes_left: 0 })
155    }
156
157    pub fn reset(&mut self) {
158        self.is_last = false;
159        self.code = 0;
160        self.esc.fill(0);
161        self.esc_cur.fill(0);
162        self.pos = 0;
163        self.esc_pos = 0;
164        self.buf.clear();
165    }
166}
167
168impl EhloResponse<String> {
169    pub fn parse(bytes: &mut Iter<'_, u8>) -> Result<Self, Error> {
170        let mut parser = Rfc5321Parser::new(bytes);
171        let mut response = EhloResponse::default();
172        let mut eol = false;
173        let mut is_first_line = true;
174
175        while !eol {
176            let mut code: u16 = 0;
177            for _ in 0..3 {
178                match parser.read_char()? {
179                    ch @ b'0'..=b'9' => {
180                        code = code.saturating_mul(10).saturating_add((ch - b'0') as u16);
181                    }
182                    _ => {
183                        return Err(Error::SyntaxError {
184                            syntax: "unexpected token",
185                        });
186                    }
187                }
188            }
189
190            if code != 250 {
191                return Err(Error::InvalidResponse { code });
192            }
193
194            match parser.read_char()? {
195                b' ' => {
196                    eol = true;
197                }
198                b'-' => (),
199                b'\n' if code < 600 => {
200                    break;
201                }
202                _ => {
203                    return Err(Error::SyntaxError {
204                        syntax: "unexpected token",
205                    });
206                }
207            }
208
209            if !is_first_line {
210                response.capabilities |= match parser.hashed_value_long()? {
211                    _8BITMIME => EXT_8BIT_MIME,
212                    ATRN => EXT_ATRN,
213                    AUTH => {
214                        while parser.stop_char != LF {
215                            if let Some(mechanism) = parser.mechanism()? {
216                                response.auth_mechanisms |= mechanism;
217                            }
218                        }
219
220                        EXT_AUTH
221                    }
222                    BINARYMIME => EXT_BINARY_MIME,
223                    BURL => EXT_BURL,
224                    CHECKPOINT => EXT_CHECKPOINT,
225                    CHUNKING => EXT_CHUNKING,
226                    CONNEG => EXT_CONNEG,
227                    CONPERM => EXT_CONPERM,
228                    DELIVERBY => {
229                        response.deliver_by = if parser.stop_char != LF {
230                            let db = parser.size()?;
231                            if db != usize::MAX { db as u64 } else { 0 }
232                        } else {
233                            0
234                        };
235                        EXT_DELIVER_BY
236                    }
237                    DSN => EXT_DSN,
238                    ENHANCEDSTATUSCO
239                        if parser.stop_char.eq_ignore_ascii_case(&b'D')
240                            && parser.read_char()?.eq_ignore_ascii_case(&b'E')
241                            && parser.read_char()?.eq_ignore_ascii_case(&b'S') =>
242                    {
243                        EXT_ENHANCED_STATUS_CODES
244                    }
245                    ETRN => EXT_ETRN,
246                    EXPN => EXT_EXPN,
247                    VRFY => EXT_VRFY,
248                    FUTURERELEASE => {
249                        let max_interval = if parser.stop_char != LF {
250                            parser.size()?
251                        } else {
252                            0
253                        };
254                        let max_datetime = if parser.stop_char != LF {
255                            parser.size()?
256                        } else {
257                            0
258                        };
259
260                        response.future_release_interval = if max_interval != usize::MAX {
261                            max_interval as u64
262                        } else {
263                            0
264                        };
265                        response.future_release_datetime = if max_datetime != usize::MAX {
266                            max_datetime as u64
267                        } else {
268                            0
269                        };
270                        EXT_FUTURE_RELEASE
271                    }
272                    HELP => EXT_HELP,
273                    MT_PRIORITY => {
274                        response.mt_priority = if parser.stop_char != LF {
275                            match parser.hashed_value_long()? {
276                                MIXER => MtPriority::Mixer,
277                                STANAG4406 => MtPriority::Stanag4406,
278                                NSEP => MtPriority::Nsep,
279                                _ => MtPriority::Mixer,
280                            }
281                        } else {
282                            MtPriority::Mixer
283                        };
284                        EXT_MT_PRIORITY
285                    }
286                    MTRK => EXT_MTRK,
287                    NO_SOLICITING => {
288                        response.no_soliciting = if parser.stop_char != LF {
289                            let text = parser.text()?;
290                            if !text.is_empty() {
291                                text.into_owned().into()
292                            } else {
293                                None
294                            }
295                        } else {
296                            None
297                        };
298                        EXT_NO_SOLICITING
299                    }
300                    ONEX => EXT_ONEX,
301                    PIPELINING => EXT_PIPELINING,
302                    REQUIRETLS => EXT_REQUIRE_TLS,
303                    RRVS => EXT_RRVS,
304                    SIZE => {
305                        response.size = if parser.stop_char != LF {
306                            let size = parser.size()?;
307                            if size != usize::MAX { size } else { 0 }
308                        } else {
309                            0
310                        };
311                        EXT_SIZE
312                    }
313                    SMTPUTF8 => EXT_SMTP_UTF8,
314                    STARTTLS => EXT_START_TLS,
315                    VERB => EXT_VERB,
316                    _ => 0,
317                };
318                parser.seek_lf()?;
319            } else {
320                let mut buf = Vec::with_capacity(16);
321                loop {
322                    match parser.read_char()? {
323                        b'\n' => break,
324                        b'\r' => (),
325                        b' ' => {
326                            parser.seek_lf()?;
327                            break;
328                        }
329                        ch if buf.len() < MAX_RESPONSE_LENGTH => {
330                            buf.push(ch);
331                        }
332                        _ => return Err(Error::ResponseTooLong),
333                    }
334                }
335                is_first_line = false;
336                response.hostname = buf.into_string();
337            }
338        }
339
340        Ok(response)
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use crate::*;
347
348    use super::ResponseReceiver;
349
350    #[test]
351    fn parse_ehlo() {
352        for item in [
353            (
354                concat!(
355                    "250-dbc.mtview.ca.us says hello\n",
356                    "250-8BITMIME\n",
357                    "250-ATRN\n",
358                    "250-AUTH GSSAPI DIGEST-MD5 PLAIN\n",
359                    "250-BINARYMIME\n",
360                    "250-BURL imap\n",
361                    "250-CHECKPOINT\n",
362                    "250-CHUNKING\n",
363                    "250-CONNEG\n",
364                    "250-CONPERM\n",
365                    "250-DELIVERBY\n",
366                    "250-DSN\n",
367                    "250-ENHANCEDSTATUSCODES\n",
368                    "250-ETRN\n",
369                    "250-EXPN\n",
370                    "250-VRFY\n",
371                    "250-FUTURERELEASE 1234 5678\n",
372                    "250-HELP\n",
373                    "250-MT-PRIORITY\n",
374                    "250-MTRK\n",
375                    "250-NO-SOLICITING net.example:ADV\n",
376                    "250-PIPELINING\n",
377                    "250-REQUIRETLS\n",
378                    "250-RRVS\n",
379                    "250-SIZE 1000000\n",
380                    "250-SMTPUTF8 ignore\n",
381                    "250 STARTTLS\n",
382                ),
383                Ok(EhloResponse {
384                    hostname: "dbc.mtview.ca.us".to_string(),
385                    capabilities: EXT_8BIT_MIME
386                        | EXT_ATRN
387                        | EXT_AUTH
388                        | EXT_BINARY_MIME
389                        | EXT_BURL
390                        | EXT_CHECKPOINT
391                        | EXT_CHUNKING
392                        | EXT_CONNEG
393                        | EXT_CONPERM
394                        | EXT_DELIVER_BY
395                        | EXT_DSN
396                        | EXT_ENHANCED_STATUS_CODES
397                        | EXT_ETRN
398                        | EXT_EXPN
399                        | EXT_VRFY
400                        | EXT_FUTURE_RELEASE
401                        | EXT_HELP
402                        | EXT_MT_PRIORITY
403                        | EXT_MTRK
404                        | EXT_NO_SOLICITING
405                        | EXT_PIPELINING
406                        | EXT_REQUIRE_TLS
407                        | EXT_RRVS
408                        | EXT_SIZE
409                        | EXT_SMTP_UTF8
410                        | EXT_START_TLS,
411                    auth_mechanisms: AUTH_GSSAPI | AUTH_DIGEST_MD5 | AUTH_PLAIN,
412                    deliver_by: 0,
413                    future_release_interval: 1234,
414                    future_release_datetime: 5678,
415                    mt_priority: MtPriority::Mixer,
416                    no_soliciting: Some("net.example:ADV".to_string()),
417                    size: 1000000,
418                }),
419            ),
420            (
421                concat!(
422                    "250-\n",
423                    "250-DELIVERBY 240\n",
424                    "250-FUTURERELEASE 123\n",
425                    "250-MT-PRIORITY MIXER\n",
426                    "250-NO-SOLICITING\n",
427                    "250-SIZE\n",
428                    "250 SMTPUTF8\n",
429                ),
430                Ok(EhloResponse {
431                    hostname: "".to_string(),
432                    capabilities: EXT_DELIVER_BY
433                        | EXT_FUTURE_RELEASE
434                        | EXT_MT_PRIORITY
435                        | EXT_NO_SOLICITING
436                        | EXT_SIZE
437                        | EXT_SMTP_UTF8,
438                    auth_mechanisms: 0,
439                    deliver_by: 240,
440                    future_release_interval: 123,
441                    future_release_datetime: 0,
442                    mt_priority: MtPriority::Mixer,
443                    no_soliciting: None,
444                    size: 0,
445                }),
446            ),
447            (
448                concat!(
449                    "250-dbc.mtview.ca.us says hello\n",
450                    "250-FUTURERELEASE\n",
451                    "250 MT-PRIORITY STANAG4406\n",
452                ),
453                Ok(EhloResponse {
454                    hostname: "dbc.mtview.ca.us".to_string(),
455                    capabilities: EXT_FUTURE_RELEASE | EXT_MT_PRIORITY,
456                    auth_mechanisms: 0,
457                    deliver_by: 0,
458                    future_release_interval: 0,
459                    future_release_datetime: 0,
460                    mt_priority: MtPriority::Stanag4406,
461                    no_soliciting: None,
462                    size: 0,
463                }),
464            ),
465            (
466                concat!("523-Massive\n", "523-Error\n", "523 Message\n"),
467                Err(Error::InvalidResponse { code: 523 }),
468            ),
469        ] {
470            let (response, parsed_response): (&str, Result<EhloResponse<String>, Error>) = item;
471
472            for replacement in ["", "\r\n", " \n", " \r\n"] {
473                let response = if !replacement.is_empty() && parsed_response.is_ok() {
474                    response.replace('\n', replacement)
475                } else {
476                    response.to_string()
477                };
478                assert_eq!(
479                    parsed_response,
480                    EhloResponse::parse(&mut response.as_bytes().iter()),
481                    "failed for {response:?}",
482                );
483            }
484        }
485    }
486
487    #[test]
488    fn parse_response() {
489        let mut all_responses = Vec::new();
490        let mut all_parsed_responses = Vec::new();
491
492        for (response, parsed_response, _) in [
493            (
494                "250 2.1.1 Originator <ned@ymir.claremont.edu> ok\n",
495                Response {
496                    code: 250,
497                    esc: [2, 1, 1],
498                    message: "Originator <ned@ymir.claremont.edu> ok".to_string(),
499                },
500                true,
501            ),
502            (
503                concat!(
504                    "551-5.7.1 Forwarding to remote hosts disabled\n",
505                    "551 5.7.1 Select another host to act as your forwarder\n"
506                ),
507                Response {
508                    code: 551,
509                    esc: [5, 7, 1],
510                    message: concat!(
511                        "Forwarding to remote hosts disabled\n",
512                        "Select another host to act as your forwarder"
513                    )
514                    .to_string(),
515                },
516                true,
517            ),
518            (
519                concat!(
520                    "550-mailbox unavailable\n",
521                    "550 user has moved with no forwarding address\n"
522                ),
523                Response {
524                    code: 550,
525                    esc: [0, 0, 0],
526                    message: "mailbox unavailable\nuser has moved with no forwarding address"
527                        .to_string(),
528                },
529                false,
530            ),
531            (
532                concat!(
533                    "550-mailbox unavailable\n",
534                    "550 user has moved with no forwarding address\n"
535                ),
536                Response {
537                    code: 550,
538                    esc: [0, 0, 0],
539                    message: "mailbox unavailable\nuser has moved with no forwarding address"
540                        .to_string(),
541                },
542                true,
543            ),
544            (
545                concat!(
546                    "432-6.8.9\n",
547                    "432-6.8.9 Hello\n",
548                    "432-6.8.9 \n",
549                    "432-6.8.9 ,\n",
550                    "432-\n",
551                    "432-6\n",
552                    "432-6.\n",
553                    "432-6.8\n",
554                    "432-6.8.9\n",
555                    "432 6.8.9 World!\n"
556                ),
557                Response {
558                    code: 432,
559                    esc: [6, 8, 9],
560                    message: "\nHello\n\n,\n\n\n6\n\n6.\n\n6.8\n\n\nWorld!".to_string(),
561                },
562                true,
563            ),
564            (
565                "250 2address.org\n",
566                Response {
567                    code: 250,
568                    esc: [0, 0, 0],
569                    message: "2address.org".to_string(),
570                },
571                true,
572            ),
573            (
574                "250 100.address.org\n",
575                Response {
576                    code: 250,
577                    esc: [0, 0, 0],
578                    message: "100.address.org".to_string(),
579                },
580                true,
581            ),
582            (
583                "250 111111111111\n",
584                Response {
585                    code: 250,
586                    esc: [0, 0, 0],
587                    message: "111111111111".to_string(),
588                },
589                true,
590            ),
591            (
592                "250 99999999999999\n",
593                Response {
594                    code: 250,
595                    esc: [0, 0, 0],
596                    message: "99999999999999".to_string(),
597                },
598                true,
599            ),
600            (
601                "250 2.0.0 Message queued for delivery.\r\n",
602                Response {
603                    code: 250,
604                    esc: [2, 0, 0],
605                    message: "Message queued for delivery.".to_string(),
606                },
607                true,
608            ),
609            (
610                concat!("250-Missing space\n", "250\n", "250 Ignore this"),
611                Response {
612                    code: 250,
613                    esc: [0, 0, 0],
614                    message: "Missing space\n".to_string(),
615                },
616                true,
617            ),
618        ] {
619            assert_eq!(
620                parsed_response,
621                ResponseReceiver::default()
622                    .parse(&mut response.as_bytes().iter())
623                    .unwrap(),
624                "failed for {response:?}",
625            );
626            all_responses.extend_from_slice(response.as_bytes());
627            all_parsed_responses.push(parsed_response);
628        }
629
630        // Test receiver
631        for chunk_size in [5, 10, 20, 30, 40, 50, 60] {
632            let mut receiver = ResponseReceiver::default();
633            let mut parsed_response = all_parsed_responses.clone().into_iter();
634            for chunk in all_responses.chunks(chunk_size) {
635                let mut bytes = chunk.iter();
636                loop {
637                    match receiver.parse(&mut bytes) {
638                        Ok(response) => {
639                            assert_eq!(
640                                parsed_response.next(),
641                                Some(response),
642                                "chunk size {chunk_size}",
643                            );
644                            receiver.reset();
645                        }
646                        Err(Error::NeedsMoreData { .. }) => {
647                            break;
648                        }
649                        err => panic!("Unexpected error {err:?} for chunk size {chunk_size}"),
650                    }
651                }
652            }
653        }
654    }
655}