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 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))]
154pub 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) => {}; (@, 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 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 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 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#[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 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 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 #[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 #[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 #[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}