smtp_message/
reply.rs

1use std::{convert::TryInto, fmt, io::IoSlice, iter, str};
2
3use lazy_static::lazy_static;
4use nom::{
5    branch::alt,
6    bytes::streaming::{tag, take},
7    combinator::{map, opt, peek, value, verify},
8    multi::many0,
9    sequence::{pair, preceded, terminated, tuple},
10    IResult,
11};
12use regex_automata::{Regex, RegexBuilder};
13
14use crate::*;
15
16lazy_static! {
17    static ref REPLY_CODE: Regex = RegexBuilder::new()
18        .anchored(true)
19        .build(r#"[2-5][0-9][0-9]"#)
20        .unwrap();
21    static ref EXTENDED_REPLY_CODE: Regex = RegexBuilder::new()
22        .anchored(true)
23        .build(r#"[245]\.[0-9]{1,3}\.[0-9]{1,3}"#)
24        .unwrap();
25    static ref REPLY_TEXT_ASCII: Regex = RegexBuilder::new()
26        .anchored(true)
27        .build(r#"[\t -~]*"#)
28        .unwrap();
29    static ref REPLY_TEXT_UTF8: Regex = RegexBuilder::new()
30        .anchored(true)
31        .build(r#"[\t -~[:^ascii:]]*"#)
32        .unwrap();
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum ReplyCodeKind {
37    PositiveCompletion,
38    PositiveIntermediate,
39    TransientNegative,
40    PermanentNegative,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum ReplyCodeCategory {
45    Syntax,
46    Information,
47    Connection,
48    ReceiverStatus,
49    Unspecified,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
54pub struct ReplyCode(pub [u8; 3]);
55
56#[rustfmt::skip]
57impl ReplyCode {
58    pub const SYSTEM_STATUS: ReplyCode = ReplyCode(*b"211");
59    pub const HELP_MESSAGE: ReplyCode = ReplyCode(*b"214");
60    pub const SERVICE_READY: ReplyCode = ReplyCode(*b"220");
61    pub const CLOSING_CHANNEL: ReplyCode = ReplyCode(*b"221");
62    pub const OKAY: ReplyCode = ReplyCode(*b"250");
63    pub const USER_NOT_LOCAL_WILL_FORWARD: ReplyCode = ReplyCode(*b"251");
64    pub const CANNOT_VRFY_BUT_PLEASE_TRY: ReplyCode = ReplyCode(*b"252");
65    pub const START_MAIL_INPUT: ReplyCode = ReplyCode(*b"354");
66    pub const SERVICE_NOT_AVAILABLE: ReplyCode = ReplyCode(*b"421");
67    pub const MAILBOX_TEMPORARILY_UNAVAILABLE: ReplyCode = ReplyCode(*b"450");
68    pub const LOCAL_ERROR: ReplyCode = ReplyCode(*b"451");
69    pub const INSUFFICIENT_STORAGE: ReplyCode = ReplyCode(*b"452");
70    pub const UNABLE_TO_ACCEPT_PARAMETERS: ReplyCode = ReplyCode(*b"455");
71    pub const COMMAND_UNRECOGNIZED: ReplyCode = ReplyCode(*b"500");
72    pub const SYNTAX_ERROR: ReplyCode = ReplyCode(*b"501");
73    pub const COMMAND_UNIMPLEMENTED: ReplyCode = ReplyCode(*b"502");
74    pub const BAD_SEQUENCE: ReplyCode = ReplyCode(*b"503");
75    pub const PARAMETER_UNIMPLEMENTED: ReplyCode = ReplyCode(*b"504");
76    pub const SERVER_DOES_NOT_ACCEPT_MAIL: ReplyCode = ReplyCode(*b"521");
77    pub const MAILBOX_UNAVAILABLE: ReplyCode = ReplyCode(*b"550");
78    pub const POLICY_REASON: ReplyCode = ReplyCode(*b"550");
79    pub const USER_NOT_LOCAL: ReplyCode = ReplyCode(*b"551");
80    pub const EXCEEDED_STORAGE: ReplyCode = ReplyCode(*b"552");
81    pub const MAILBOX_NAME_INCORRECT: ReplyCode = ReplyCode(*b"553");
82    pub const TRANSACTION_FAILED: ReplyCode = ReplyCode(*b"554");
83    pub const MAIL_OR_RCPT_PARAMETER_UNIMPLEMENTED: ReplyCode = ReplyCode(*b"555");
84    pub const DOMAIN_DOES_NOT_ACCEPT_MAIL: ReplyCode = ReplyCode(*b"556");
85}
86
87impl ReplyCode {
88    #[inline]
89    pub fn parse(buf: &[u8]) -> IResult<&[u8], ReplyCode> {
90        map(apply_regex(&REPLY_CODE), |b| {
91            // The below unwrap is OK, as the regex already validated
92            // that there are exactly 3 characters
93            ReplyCode(b.try_into().unwrap())
94        })(buf)
95    }
96
97    #[inline]
98    pub fn kind(&self) -> ReplyCodeKind {
99        match self.0[0] {
100            b'2' => ReplyCodeKind::PositiveCompletion,
101            b'3' => ReplyCodeKind::PositiveIntermediate,
102            b'4' => ReplyCodeKind::TransientNegative,
103            b'5' => ReplyCodeKind::PermanentNegative,
104            _ => panic!("Asked kind of invalid reply code!"),
105        }
106    }
107
108    #[inline]
109    pub fn category(&self) -> ReplyCodeCategory {
110        match self.0[1] {
111            b'0' => ReplyCodeCategory::Syntax,
112            b'1' => ReplyCodeCategory::Information,
113            b'2' => ReplyCodeCategory::Connection,
114            b'5' => ReplyCodeCategory::ReceiverStatus,
115            _ => ReplyCodeCategory::Unspecified,
116        }
117    }
118
119    #[inline]
120    pub fn code(&self) -> u16 {
121        self.0[0] as u16 * 100 + self.0[1] as u16 * 10 + self.0[2] as u16 - b'0' as u16 * 111
122    }
123
124    #[inline]
125    pub fn as_io_slices(&self) -> impl Iterator<Item = IoSlice> {
126        iter::once(IoSlice::new(&self.0))
127    }
128}
129
130#[derive(Copy, Clone, Debug, PartialEq, Eq)]
131#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
132#[repr(u8)]
133pub enum EnhancedReplyCodeClass {
134    Success = 2,
135    PersistentTransient = 4,
136    PermanentFailure = 5,
137}
138
139#[derive(Copy, Clone, Debug, PartialEq, Eq)]
140#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
141pub enum EnhancedReplyCodeSubject {
142    Undefined,
143    Addressing,
144    Mailbox,
145    MailSystem,
146    Network,
147    MailDelivery,
148    Content,
149    Policy,
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
153#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
154// Note: S is here always ascii-only, as we know the regex it matches
155pub struct EnhancedReplyCode<S> {
156    pub raw: S,
157    pub class: EnhancedReplyCodeClass,
158    pub raw_subject: u16,
159    pub raw_detail: u16,
160}
161
162macro_rules! extended_reply_codes {
163    ($(($success:tt, $transient:tt, $permanent:tt, $subject:tt, $detail:tt),)*) => {
164        $(
165            extended_reply_codes!(@, success, $success, $subject, $detail);
166            extended_reply_codes!(@, transient, $transient, $subject, $detail);
167            extended_reply_codes!(@, permanent, $permanent, $subject, $detail);
168        )*
169    };
170
171    (@, $any:ident, _, $subject:tt, $detail:tt) => {}; // ignore these
172
173    (@, success, $success:ident, $subject:tt, $detail:tt) => {
174        pub const $success: EnhancedReplyCode<&'static str> = EnhancedReplyCode {
175            raw: concat!("2.", stringify!($subject), ".", stringify!($detail)),
176            class: EnhancedReplyCodeClass::Success,
177            raw_subject: $subject,
178            raw_detail: $detail,
179        };
180    };
181
182    (@, transient, $transient:ident, $subject:tt, $detail:tt) => {
183        pub const $transient: EnhancedReplyCode<&'static str> = EnhancedReplyCode {
184            raw: concat!("4.", stringify!($subject), ".", stringify!($detail)),
185            class: EnhancedReplyCodeClass::PersistentTransient,
186            raw_subject: $subject,
187            raw_detail: $detail,
188        };
189    };
190
191    (@, permanent, $permanent:ident, $subject:tt, $detail:tt) => {
192        pub const $permanent: EnhancedReplyCode<&'static str> = EnhancedReplyCode {
193            raw: concat!("5.", stringify!($subject), ".", stringify!($detail)),
194            class: EnhancedReplyCodeClass::PermanentFailure,
195            raw_subject: $subject,
196            raw_detail: $detail,
197        };
198    };
199}
200
201#[rustfmt::skip]
202impl EnhancedReplyCode<&'static str> {
203    extended_reply_codes!(
204        (SUCCESS_UNDEFINED, TRANSIENT_UNDEFINED, PERMANENT_UNDEFINED, 0, 0),
205
206        (SUCCESS_ADDRESS_OTHER, TRANSIENT_ADDRESS_OTHER, PERMANENT_ADDRESS_OTHER, 1, 0),
207        (_, _, PERMANENT_BAD_DEST_MAILBOX, 1, 1),
208        (_, _, PERMANENT_BAD_DEST_SYSTEM, 1, 2),
209        (_, _, PERMANENT_BAD_DEST_MAILBOX_SYNTAX, 1, 3),
210        (SUCCESS_DEST_MAILBOX_AMBIGUOUS, TRANSIENT_DEST_MAILBOX_AMBIGUOUS, PERMANENT_DEST_MAILBOX_AMBIGUOUS, 1, 4),
211        (SUCCESS_DEST_VALID, _, _, 1, 5),
212        (_, _, PERMANENT_DEST_MAILBOX_HAS_MOVED, 1, 6),
213        (_, _, PERMANENT_BAD_SENDER_MAILBOX_SYNTAX, 1, 7),
214        (_, TRANSIENT_BAD_SENDER_SYSTEM, PERMANENT_BAD_SENDER_SYSTEM, 1, 8),
215        (SUCCESS_MESSAGE_RELAYED_TO_NON_COMPLIANT_MAILER, _, PERMANENT_MESSAGE_RELAYED_TO_NON_COMPLIANT_MAILER, 1, 9),
216        (_, _, PERMANENT_RECIPIENT_ADDRESS_HAS_NULL_MX, 1, 10),
217
218        (SUCCESS_MAILBOX_OTHER, TRANSIENT_MAILBOX_OTHER, PERMANENT_MAILBOX_OTHER, 2, 0),
219        (_, TRANSIENT_MAILBOX_DISABLED, PERMANENT_MAILBOX_DISABLED, 2, 1),
220        (_, TRANSIENT_MAILBOX_FULL, _, 2, 2),
221        (_, _, PERMANENT_MESSAGE_TOO_LONG_FOR_MAILBOX, 2, 3),
222        (_, TRANSIENT_MAILING_LIST_EXPANSION_ISSUE, PERMANENT_MAILING_LIST_EXPANSION_ISSUE, 2, 4),
223
224        (SUCCESS_SYSTEM_OTHER, TRANSIENT_SYSTEM_OTHER, PERMANENT_SYSTEM_OTHER, 3, 0),
225        (_, TRANSIENT_SYSTEM_FULL, _, 3, 1),
226        (_, TRANSIENT_SYSTEM_NOT_ACCEPTING_MESSAGES, PERMANENT_SYSTEM_NOT_ACCEPTING_MESSAGES, 3, 2),
227        (_, TRANSIENT_SYSTEM_INCAPABLE_OF_FEATURE, PERMANENT_SYSTEM_INCAPABLE_OF_FEATURE, 3, 3),
228        (_, _, PERMANENT_MESSAGE_TOO_BIG, 3, 4),
229        (_, TRANSIENT_SYSTEM_INCORRECTLY_CONFIGURED, PERMANENT_SYSTEM_INCORRECTLY_CONFIGURED, 3, 5),
230        (SUCCESS_REQUESTED_PRIORITY_WAS_CHANGED, _, _, 3, 6),
231
232        (SUCCESS_NETWORK_OTHER, TRANSIENT_NETWORK_OTHER, PERMANENT_NETWORK_OTHER, 4, 0),
233        (_, TRANSIENT_NO_ANSWER_FROM_HOST, _, 4, 1),
234        (_, TRANSIENT_BAD_CONNECTION, _, 4, 2),
235        (_, TRANSIENT_DIRECTORY_SERVER_FAILURE, _, 4, 3),
236        (_, TRANSIENT_UNABLE_TO_ROUTE, PERMANENT_UNABLE_TO_ROUTE, 4, 4),
237        (_, TRANSIENT_SYSTEM_CONGESTION, _, 4, 5),
238        (_, TRANSIENT_ROUTING_LOOP_DETECTED, _, 4, 6),
239        (_, TRANSIENT_DELIVERY_TIME_EXPIRED, PERMANENT_DELIVERY_TIME_EXPIRED, 4, 7),
240
241        (SUCCESS_DELIVERY_OTHER, TRANSIENT_DELIVERY_OTHER, PERMANENT_DELIVERY_OTHER, 5, 0),
242        (_, _, PERMANENT_INVALID_COMMAND, 5, 1),
243        (_, _, PERMANENT_SYNTAX_ERROR, 5, 2),
244        (_, TRANSIENT_TOO_MANY_RECIPIENTS, PERMANENT_TOO_MANY_RECIPIENTS, 5, 3),
245        (_, _, PERMANENT_INVALID_COMMAND_ARGUMENTS, 5, 4),
246        (_, TRANSIENT_WRONG_PROTOCOL_VERSION, PERMANENT_WRONG_PROTOCOL_VERSION, 5, 5),
247        (_, TRANSIENT_AUTH_EXCHANGE_LINE_TOO_LONG, PERMANENT_AUTH_EXCHANGE_LINE_TOO_LONG, 5, 6),
248
249        (SUCCESS_CONTENT_OTHER, TRANSIENT_CONTENT_OTHER, PERMANENT_CONTENT_OTHER, 6, 0),
250        (_, _, PERMANENT_MEDIA_NOT_SUPPORTED, 6, 1),
251        (_, TRANSIENT_CONVERSION_REQUIRED_AND_PROHIBITED, PERMANENT_CONVERSION_REQUIRED_AND_PROHIBITED, 6, 2),
252        (_, TRANSIENT_CONVERSION_REQUIRED_BUT_NOT_SUPPORTED, PERMANENT_CONVERSION_REQUIRED_BUT_NOT_SUPPORTED, 6, 3),
253        (SUCCESS_CONVERSION_WITH_LOSS_PERFORMED, TRANSIENT_CONVERSION_WITH_LOSS_PERFORMED, PERMANENT_CONVERSION_WITH_LOSS_PERFORMED, 6, 4),
254        (_, TRANSIENT_CONVERSION_FAILED, PERMANENT_CONVERSION_FAILED, 6, 5),
255        (_, TRANSIENT_MESSAGE_CONTENT_NOT_AVAILABLE, PERMANENT_MESSAGE_CONTENT_NOT_AVAILABLE, 6, 6),
256        (_, _, PERMANENT_NON_ASCII_ADDRESSES_NOT_PERMITTED, 6, 7),
257        (SUCCESS_UTF8_WOULD_BE_REQUIRED, TRANSIENT_UTF8_WOULD_BE_REQUIRED, PERMANENT_UTF8_WOULD_BE_REQUIRED, 6, 8),
258        (_, _, PERMANENT_UTF8_MESSAGE_CANNOT_BE_TRANSMITTED, 6, 9),
259        (SUCCESS_UTF8_WOULD_BE_REQUIRED_BIS, TRANSIENT_UTF8_WOULD_BE_REQUIRED_BIS, PERMANENT_UTF8_WOULD_BE_REQUIRED_BIS, 6, 10),
260
261        (SUCCESS_POLICY_OTHER, TRANSIENT_POLICY_OTHER, PERMANENT_POLICY_OTHER, 7, 0),
262        (_, _, PERMANENT_DELIVERY_NOT_AUTHORIZED, 7, 1),
263        (_, _, PERMANENT_MAILING_LIST_EXPANSION_PROHIBITED, 7, 2),
264        (_, _, PERMANENT_SECURITY_CONVERSION_REQUIRED_BUT_NOT_POSSIBLE, 7, 3),
265        (_, _, PERMANENT_SECURITY_FEATURES_NOT_SUPPORTED, 7, 4),
266        (_, TRANSIENT_CRYPTO_FAILURE, PERMANENT_CRYPTO_FAILURE, 7, 5),
267        (_, TRANSIENT_CRYPTO_ALGO_NOT_SUPPORTED, PERMANENT_CRYPTO_ALGO_NOT_SUPPORTED, 7, 6),
268        (SUCCESS_MESSAGE_INTEGRITY_FAILURE, TRANSIENT_MESSAGE_INTEGRITY_FAILURE, PERMANENT_MESSAGE_INTEGRITY_FAILURE, 7, 7),
269        (_, _, PERMANENT_AUTH_CREDENTIALS_INVALID, 7, 8),
270        (_, _, PERMANENT_AUTH_MECHANISM_TOO_WEAK, 7, 9),
271        (_, _, PERMANENT_ENCRYPTION_NEEDED, 7, 10),
272        (_, _, PERMANENT_ENCRYPTION_REQUIRED_FOR_REQUESTED_AUTH_MECHANISM, 7, 11),
273        (_, TRANSIENT_PASSWORD_TRANSITION_NEEDED, _, 7, 12),
274        (_, _, PERMANENT_USER_ACCOUNT_DISABLED, 7, 13),
275        (_, _, PERMANENT_TRUST_RELATIONSHIP_REQUIRED, 7, 14),
276        (_, TRANSIENT_PRIORITY_TOO_LOW, PERMANENT_PRIORITY_TOO_LOW, 7, 15),
277        (_, TRANSIENT_MESSAGE_TOO_BIG_FOR_PRIORITY, PERMANENT_MESSAGE_TOO_BIG_FOR_PRIORITY, 7, 16),
278        (_, _, PERMANENT_MAILBOX_OWNER_HAS_CHANGED, 7, 17),
279        (_, _, PERMANENT_DOMAIN_OWNER_HAS_CHANGED, 7, 18),
280        (_, _, PERMANENT_RRVS_CANNOT_BE_COMPLETED, 7, 19),
281        (_, _, PERMANENT_NO_PASSING_DKIM_SIGNATURE_FOUND, 7, 20),
282        (_, _, PERMANENT_NO_ACCEPTABLE_DKIM_SIGNATURE_FOUND, 7, 21),
283        (_, _, PERMANENT_NO_AUTHOR_MATCHED_DKIM_SIGNATURE_FOUND, 7, 22),
284        (_, _, PERMANENT_SPF_VALIDATION_FAILED, 7, 23),
285        (_, TRANSIENT_SPF_VALIDATION_ERROR, PERMANENT_SPF_VALIDATION_ERROR, 7, 24),
286        (_, _, PERMANENT_REVERSE_DNS_VALIDATION_FAILED, 7, 25),
287        (_, _, PERMANENT_MULTIPLE_AUTH_CHECKS_FAILED, 7, 26),
288        (_, _, PERMANENT_SENDER_ADDRESS_HAS_NULL_MX, 7, 27),
289        (SUCCESS_MAIL_FLOOD_DETECTED, TRANSIENT_MAIL_FLOOD_DETECTED, PERMANENT_MAIL_FLOOD_DETECTED, 7, 28),
290        (_, _, PERMANENT_ARC_VALIDATION_FAILURE, 7, 29),
291        (_, _, PERMANENT_REQUIRETLS_SUPPORT_REQUIRED, 7, 30),
292    );
293}
294
295impl<S> EnhancedReplyCode<S> {
296    pub fn parse<'a>(buf: &'a [u8]) -> IResult<&'a [u8], EnhancedReplyCode<S>>
297    where
298        S: From<&'a str>,
299    {
300        map(apply_regex(&EXTENDED_REPLY_CODE), |raw| {
301            let class = raw[0] - b'0';
302            let class = match class {
303                2 => EnhancedReplyCodeClass::Success,
304                4 => EnhancedReplyCodeClass::PersistentTransient,
305                5 => EnhancedReplyCodeClass::PermanentFailure,
306                _ => panic!("Regex allowed unexpected elements"),
307            };
308            let after_class = &raw[2..];
309            // These unwrap and unsafe are OK thanks to the regex
310            // already matching
311            let second_dot = after_class.iter().position(|c| *c == b'.').unwrap();
312            let raw_subject = unsafe { str::from_utf8_unchecked(&after_class[..second_dot]) }
313                .parse()
314                .unwrap();
315            let raw_detail = unsafe { str::from_utf8_unchecked(&after_class[second_dot + 1..]) }
316                .parse()
317                .unwrap();
318            let raw = unsafe { str::from_utf8_unchecked(raw) };
319            EnhancedReplyCode {
320                raw: raw.into(),
321                class,
322                raw_subject,
323                raw_detail,
324            }
325        })(buf)
326    }
327
328    #[inline]
329    pub fn subject(&self) -> EnhancedReplyCodeSubject {
330        match self.raw_subject {
331            1 => EnhancedReplyCodeSubject::Addressing,
332            2 => EnhancedReplyCodeSubject::Mailbox,
333            3 => EnhancedReplyCodeSubject::MailSystem,
334            4 => EnhancedReplyCodeSubject::Network,
335            5 => EnhancedReplyCodeSubject::MailDelivery,
336            6 => EnhancedReplyCodeSubject::Content,
337            7 => EnhancedReplyCodeSubject::Policy,
338            _ => EnhancedReplyCodeSubject::Undefined,
339        }
340    }
341
342    #[inline]
343    pub fn into<T>(self) -> EnhancedReplyCode<T>
344    where
345        T: From<S>,
346    {
347        EnhancedReplyCode {
348            raw: self.raw.into(),
349            class: self.class,
350            raw_subject: self.raw_subject,
351            raw_detail: self.raw_detail,
352        }
353    }
354}
355
356impl<S> EnhancedReplyCode<S>
357where
358    S: AsRef<str>,
359{
360    #[inline]
361    pub fn as_io_slices(&self) -> impl Iterator<Item = IoSlice> {
362        iter::once(IoSlice::new(self.raw.as_ref().as_ref()))
363    }
364}
365
366impl EnhancedReplyCode<&str> {
367    pub fn to_owned(&self) -> EnhancedReplyCode<String> {
368        EnhancedReplyCode {
369            raw: self.raw.to_owned(),
370            class: self.class,
371            raw_subject: self.raw_subject,
372            raw_detail: self.raw_detail,
373        }
374    }
375}
376
377impl<T> EnhancedReplyCode<T> {
378    pub fn convert<U>(self) -> EnhancedReplyCode<U>
379    where
380        U: From<T>,
381    {
382        EnhancedReplyCode {
383            raw: self.raw.into(),
384            class: self.class,
385            raw_subject: self.raw_subject,
386            raw_detail: self.raw_detail,
387        }
388    }
389}
390
391#[derive(Clone, Debug, PartialEq, Eq)]
392#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
393pub struct ReplyLine<S> {
394    pub code: ReplyCode,
395    pub last: bool,
396    pub ecode: Option<EnhancedReplyCode<S>>,
397    pub text: MaybeUtf8<S>,
398}
399
400impl<S> ReplyLine<S> {
401    pub fn parse<'a>(buf: &'a [u8]) -> IResult<&'a [u8], ReplyLine<S>>
402    where
403        S: From<&'a str>,
404    {
405        map(
406            tuple((
407                ReplyCode::parse,
408                alt((value(false, tag(b"-")), value(true, opt(tag(b" "))))),
409                opt(terminated(
410                    EnhancedReplyCode::parse,
411                    alt((tag(b" "), peek(tag(b"\r\n")))),
412                )),
413                alt((
414                    map(
415                        terminated(apply_regex(&REPLY_TEXT_ASCII), tag(b"\r\n")),
416                        |b: &[u8]| {
417                            // The below unsafe is OK, thanks to our
418                            // regex validating that `b` is proper
419                            // ascii (and thus utf-8)
420                            let s = unsafe { str::from_utf8_unchecked(b) };
421                            MaybeUtf8::Ascii(s.into())
422                        },
423                    ),
424                    map(
425                        terminated(apply_regex(&REPLY_TEXT_UTF8), tag(b"\r\n")),
426                        |b: &[u8]| {
427                            // The below unsafe is OK, thanks to our
428                            // regex validating that `b` is proper
429                            // utf8
430                            let s = unsafe { str::from_utf8_unchecked(b) };
431                            MaybeUtf8::Utf8(s.into())
432                        },
433                    ),
434                )),
435            )),
436            |(code, last, ecode, text)| ReplyLine {
437                code,
438                last,
439                ecode,
440                text,
441            },
442        )(buf)
443    }
444}
445
446#[inline]
447fn line_as_io_slices<'a, S>(
448    code: &'a ReplyCode,
449    last: bool,
450    ecode: &'a Option<EnhancedReplyCode<S>>,
451    text: &'a MaybeUtf8<S>,
452) -> impl 'a + Iterator<Item = IoSlice<'a>>
453where
454    S: AsRef<str>,
455{
456    let is_last_char = match last {
457        true => b" ",
458        false => b"-",
459    };
460    code.as_io_slices()
461        .chain(iter::once(IoSlice::new(is_last_char)))
462        .chain(
463            ecode
464                .iter()
465                .flat_map(|c| c.as_io_slices().chain(iter::once(IoSlice::new(b" ")))),
466        )
467        .chain(text.as_io_slices())
468        .chain(iter::once(IoSlice::new(b"\r\n")))
469}
470
471impl<S> ReplyLine<S>
472where
473    S: AsRef<str>,
474{
475    #[inline]
476    pub fn as_io_slices(&self) -> impl Iterator<Item = IoSlice> {
477        line_as_io_slices(&self.code, self.last, &self.ecode, &self.text)
478    }
479}
480
481// TODO: use ascii crate for From<&'a AsciiStr> instead of From<&'a
482// str> for the ascii variants
483
484#[derive(Clone, Debug, Eq, PartialEq)]
485#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
486pub struct Reply<S = String> {
487    pub code: ReplyCode,
488    pub ecode: Option<EnhancedReplyCode<S>>,
489    // TODO: should we try to make constructing a constant reply noalloc?
490    pub text: Vec<MaybeUtf8<S>>,
491}
492
493impl<S> Reply<S> {
494    #[inline]
495    pub fn parse<'a>(buf: &'a [u8]) -> IResult<&'a [u8], Reply<S>>
496    where
497        S: From<&'a str>,
498    {
499        // TODO: raise yellow flags if .code and .ecode are different
500        // between the parsed reply lines
501        map(
502            pair(
503                many0(preceded(
504                    peek(pair(take(3usize), tag(b"-"))),
505                    ReplyLine::parse,
506                )),
507                verify(ReplyLine::parse, |l| l.last),
508            ),
509            |(beg, end)| Reply {
510                code: end.code,
511                ecode: end.ecode,
512                text: beg
513                    .into_iter()
514                    .map(|l| l.text)
515                    .chain(iter::once(end.text))
516                    .collect(),
517            },
518        )(buf)
519    }
520}
521
522impl<S> Reply<S>
523where
524    S: AsRef<str>,
525{
526    #[inline]
527    pub fn as_io_slices(&self) -> impl Iterator<Item = IoSlice> {
528        let code = &self.code;
529        let ecode = &self.ecode;
530        let last_i = self.text.len() - 1;
531        self.text
532            .iter()
533            .enumerate()
534            .flat_map(move |(i, l)| line_as_io_slices(code, i == last_i, ecode, l))
535    }
536}
537
538impl<S> fmt::Display for Reply<S>
539where
540    S: AsRef<str>,
541{
542    #[inline]
543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544        for s in self.as_io_slices() {
545            write!(f, "{}", String::from_utf8_lossy(&s))?;
546        }
547        Ok(())
548    }
549}
550
551impl Reply<&str> {
552    #[inline]
553    pub fn into_owned(self) -> Reply<String> {
554        Reply {
555            code: self.code,
556            ecode: self.ecode.map(|c| c.to_owned()),
557            text: self.text.into_iter().map(|l| l.to_owned()).collect(),
558        }
559    }
560}
561
562impl<U> Reply<U> {
563    pub fn convert<T>(self) -> Reply<T>
564    where
565        T: From<U>,
566    {
567        Reply {
568            code: self.code,
569            ecode: self.ecode.map(|e| e.convert()),
570            text: self.text.into_iter().map(|t| t.convert()).collect(),
571        }
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    #[test]
580    fn reply_code_valid() {
581        let tests: &[(&[u8], [u8; 3])] = &[(b"523", *b"523"), (b"234", *b"234")];
582        for (inp, out) in tests {
583            println!("Test: {:?}", show_bytes(inp));
584            let r = ReplyCode::parse(inp);
585            println!("Result: {:?}", r);
586            match r {
587                Ok((rest, res)) => {
588                    assert_eq!(rest, b"");
589                    assert_eq!(res, ReplyCode(*out));
590                }
591                x => panic!("Unexpected result: {:?}", x),
592            }
593        }
594    }
595
596    #[test]
597    fn reply_code_incomplete() {
598        let tests: &[&[u8]] = &[b"3", b"43"];
599        for inp in tests {
600            let r = ReplyCode::parse(inp);
601            println!("{:?}:  {:?}", show_bytes(inp), r);
602            assert!(r.unwrap_err().is_incomplete());
603        }
604    }
605
606    #[test]
607    fn reply_code_invalid() {
608        let tests: &[&[u8]] = &[b"foo", b"123", b"648"];
609        for inp in tests {
610            let r = ReplyCode::parse(inp);
611            assert!(!r.unwrap_err().is_incomplete());
612        }
613    }
614
615    // TODO: test reply code builder
616
617    #[test]
618    pub fn extended_reply_code_valid() {
619        let tests: &[(&[u8], (EnhancedReplyCodeClass, u16, u16))] = &[
620            (b"2.1.23", (EnhancedReplyCodeClass::Success, 1, 23)),
621            (
622                b"5.243.567",
623                (EnhancedReplyCodeClass::PermanentFailure, 243, 567),
624            ),
625        ];
626        for (inp, (class, raw_subject, raw_detail)) in tests.iter().cloned() {
627            println!("Test: {:?}", show_bytes(inp));
628            let r = EnhancedReplyCode::parse(inp);
629            println!("Result: {:?}", r);
630            match r {
631                Ok((rest, res)) => {
632                    assert_eq!(rest, b"");
633                    assert_eq!(res, EnhancedReplyCode {
634                        raw: str::from_utf8(inp).unwrap(),
635                        class,
636                        raw_subject,
637                        raw_detail,
638                    });
639                }
640                x => panic!("Unexpected result: {:?}", x),
641            }
642        }
643    }
644
645    #[test]
646    fn extended_reply_code_incomplete() {
647        let tests: &[&[u8]] = &[b"4.", b"5.23"];
648        for inp in tests {
649            let r = EnhancedReplyCode::<&str>::parse(inp);
650            println!("{:?}:  {:?}", show_bytes(inp), r);
651            assert!(r.unwrap_err().is_incomplete());
652        }
653    }
654
655    #[test]
656    fn extended_reply_code_invalid() {
657        let tests: &[&[u8]] = &[b"foo", b"3.5.1", b"1.1000.2"];
658        for inp in tests {
659            let r = EnhancedReplyCode::<String>::parse(inp);
660            assert!(!r.unwrap_err().is_incomplete());
661        }
662    }
663
664    // TODO: test extended reply code builder
665
666    #[test]
667    fn reply_line_valid() {
668        let tests: &[(&[u8], ReplyLine<&str>)] = &[
669            (b"250 All is well\r\n", ReplyLine {
670                code: ReplyCode(*b"250"),
671                last: true,
672                ecode: None,
673                text: MaybeUtf8::Ascii("All is well"),
674            }),
675            (b"450-Temporary\r\n", ReplyLine {
676                code: ReplyCode(*b"450"),
677                last: false,
678                ecode: None,
679                text: MaybeUtf8::Ascii("Temporary"),
680            }),
681            (b"354 Please do start input now\r\n", ReplyLine {
682                code: ReplyCode(*b"354"),
683                last: true,
684                ecode: None,
685                text: MaybeUtf8::Ascii("Please do start input now"),
686            }),
687            (b"550 5.1.1 Mailbox does not exist\r\n", ReplyLine {
688                code: ReplyCode(*b"550"),
689                last: true,
690                ecode: Some(EnhancedReplyCode::parse(b"5.1.1").unwrap().1),
691                text: MaybeUtf8::Ascii("Mailbox does not exist"),
692            }),
693        ];
694        for (inp, out) in tests.iter().cloned() {
695            println!("Test: {:?}", show_bytes(inp));
696            let r = ReplyLine::parse(inp);
697            println!("Result: {:?}", r);
698            match r {
699                Ok((rest, res)) => {
700                    assert_eq!(rest, b"");
701                    assert_eq!(res, out);
702                }
703                x => panic!("Unexpected result: {:?}", x),
704            }
705        }
706    }
707
708    // TODO: test incomplete, invalid for ReplyLine
709
710    #[test]
711    fn reply_line_build() {
712        let tests: &[(ReplyLine<&str>, &[u8])] = &[
713            (
714                ReplyLine {
715                    code: ReplyCode::SERVICE_READY,
716                    last: false,
717                    ecode: None,
718                    text: MaybeUtf8::Ascii("hello world!"),
719                },
720                b"220-hello world!\r\n",
721            ),
722            (
723                ReplyLine {
724                    code: ReplyCode::COMMAND_UNIMPLEMENTED,
725                    last: true,
726                    ecode: None,
727                    text: MaybeUtf8::Ascii("test"),
728                },
729                b"502 test\r\n",
730            ),
731            (
732                ReplyLine {
733                    code: ReplyCode::MAILBOX_UNAVAILABLE,
734                    last: true,
735                    ecode: Some(EnhancedReplyCode::PERMANENT_BAD_DEST_MAILBOX),
736                    text: MaybeUtf8::Utf8("mélbox does not exist"),
737                },
738                "550 5.1.1 mélbox does not exist\r\n".as_bytes(),
739            ),
740            (
741                ReplyLine {
742                    code: ReplyCode::USER_NOT_LOCAL,
743                    last: false,
744                    ecode: Some(EnhancedReplyCode::PERMANENT_DELIVERY_NOT_AUTHORIZED),
745                    text: MaybeUtf8::Ascii("Forwarding is disabled"),
746                },
747                "551-5.7.1 Forwarding is disabled\r\n".as_bytes(),
748            ),
749        ];
750        for (inp, out) in tests {
751            println!("Test: {:?}", inp);
752            let res = inp
753                .as_io_slices()
754                .flat_map(|s| s.iter().cloned().collect::<Vec<_>>().into_iter())
755                .collect::<Vec<u8>>();
756            println!("Result  : {:?}", show_bytes(&res));
757            println!("Expected: {:?}", show_bytes(out));
758            assert_eq!(&res, out);
759        }
760    }
761}