1use std::io::Write;
2
3#[cfg(feature = "serdex")]
4use serde::{Deserialize, Serialize};
5
6use crate::utils::escape_quoted;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum Command {
10 Ehlo {
11 domain_or_address: DomainOrAddress,
12 },
13 Helo {
14 domain_or_address: DomainOrAddress,
15 },
16 Mail {
17 reverse_path: String,
18 parameters: Vec<Parameter>,
19 },
20 Rcpt {
21 forward_path: String,
22 parameters: Vec<Parameter>,
23 },
24 Data,
25 Rset,
26 Vrfy {
33 user_or_mailbox: AtomOrQuoted,
34 },
35 Expn {
45 mailing_list: AtomOrQuoted,
46 },
47 Help {
58 argument: Option<AtomOrQuoted>,
59 },
60 Noop {
70 argument: Option<AtomOrQuoted>,
71 },
72 Quit,
89 StartTLS,
91 AuthLogin(Option<String>),
93 AuthPlain(Option<String>),
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub enum DomainOrAddress {
99 Domain(String),
100 Address(String),
101}
102
103impl DomainOrAddress {
104 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
105 match self {
106 DomainOrAddress::Domain(domain) => write!(writer, "{}", domain),
107 DomainOrAddress::Address(address) => write!(writer, "[{}]", address),
108 }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct Parameter {
114 keyword: String,
115 value: Option<String>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum AtomOrQuoted {
120 Atom(String),
121 Quoted(String),
122}
123
124impl Command {
125 pub fn name(&self) -> &'static str {
126 match self {
127 Command::Ehlo { .. } => "EHLO",
128 Command::Helo { .. } => "HELO",
129 Command::Mail { .. } => "MAIL",
130 Command::Rcpt { .. } => "RCPT",
131 Command::Data => "DATA",
132 Command::Rset => "RSET",
133 Command::Vrfy { .. } => "VRFY",
134 Command::Expn { .. } => "EXPN",
135 Command::Help { .. } => "HELP",
136 Command::Noop { .. } => "NOOP",
137 Command::Quit => "QUIT",
138 Command::StartTLS => "STARTTLS",
140 Command::AuthLogin(_) => "AUTHLOGIN",
142 Command::AuthPlain(_) => "AUTHPLAIN",
144 }
145 }
146
147 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
148 use Command::*;
149
150 match self {
151 Helo { domain_or_address } => {
153 writer.write_all(b"HELO ")?;
154 domain_or_address.serialize(writer)?;
155 }
156 Ehlo { domain_or_address } => {
158 writer.write_all(b"EHLO ")?;
159 domain_or_address.serialize(writer)?;
160 }
161 Mail {
163 reverse_path,
164 parameters,
165 } => {
166 writer.write_all(b"MAIL FROM:<")?;
167 writer.write_all(reverse_path.as_bytes())?;
168 writer.write_all(b">")?;
169
170 for parameter in parameters {
171 writer.write_all(b" ")?;
172 parameter.serialize(writer)?;
173 }
174 }
175 Rcpt {
177 forward_path,
178 parameters,
179 } => {
180 writer.write_all(b"RCPT TO:<")?;
181 writer.write_all(forward_path.as_bytes())?;
182 writer.write_all(b">")?;
183
184 for parameter in parameters {
185 writer.write_all(b" ")?;
186 parameter.serialize(writer)?;
187 }
188 }
189 Data => writer.write_all(b"DATA")?,
191 Rset => writer.write_all(b"RSET")?,
193 Vrfy { user_or_mailbox } => {
195 writer.write_all(b"VRFY ")?;
196 user_or_mailbox.serialize(writer)?;
197 }
198 Expn { mailing_list } => {
200 writer.write_all(b"EXPN ")?;
201 mailing_list.serialize(writer)?;
202 }
203 Help { argument: None } => writer.write_all(b"HELP")?,
205 Help {
206 argument: Some(data),
207 } => {
208 writer.write_all(b"HELP ")?;
209 data.serialize(writer)?;
210 }
211 Noop { argument: None } => writer.write_all(b"NOOP")?,
213 Noop {
214 argument: Some(data),
215 } => {
216 writer.write_all(b"NOOP ")?;
217 data.serialize(writer)?;
218 }
219 Quit => writer.write_all(b"QUIT")?,
221 StartTLS => writer.write_all(b"STARTTLS")?,
224 AuthLogin(None) => {
226 writer.write_all(b"AUTH LOGIN")?;
227 }
228 AuthLogin(Some(data)) => {
229 writer.write_all(b"AUTH LOGIN ")?;
230 writer.write_all(data.as_bytes())?;
231 }
232 AuthPlain(None) => {
234 writer.write_all(b"AUTH PLAIN")?;
235 }
236 AuthPlain(Some(data)) => {
237 writer.write_all(b"AUTH PLAIN ")?;
238 writer.write_all(data.as_bytes())?;
239 }
240 }
241
242 write!(writer, "\r\n")
243 }
244}
245
246impl Parameter {
247 pub fn new<K: Into<String>, V: Into<String>>(keyword: K, value: Option<V>) -> Parameter {
248 Parameter {
249 keyword: keyword.into(),
250 value: value.map(Into::into),
251 }
252 }
253
254 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
255 writer.write_all(self.keyword.as_bytes())?;
256
257 if let Some(ref value) = self.value {
258 writer.write_all(b"=")?;
259 writer.write_all(value.as_bytes())?;
260 }
261
262 Ok(())
263 }
264}
265
266impl AtomOrQuoted {
267 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
268 match self {
269 AtomOrQuoted::Atom(atom) => {
270 writer.write_all(atom.as_bytes())?;
271 }
272 AtomOrQuoted::Quoted(quoted) => {
273 writer.write_all(b"\"")?;
274 writer.write_all(escape_quoted(quoted).as_bytes())?;
275 writer.write_all(b"\"")?;
276 }
277 }
278
279 Ok(())
280 }
281}
282
283#[cfg_attr(feature = "serdex", derive(Serialize, Deserialize))]
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub enum Response {
288 Greeting {
289 domain: String,
290 text: String,
291 },
292 Ehlo {
293 domain: String,
294 greet: Option<String>,
295 capabilities: Vec<Capability>,
296 },
297 Other {
298 code: u16,
299 text: String,
300 },
301}
302
303impl Response {
304 pub fn greeting<D, T>(domain: D, text: T) -> Response
305 where
306 D: Into<String>,
307 T: Into<String>,
308 {
309 Response::Greeting {
310 domain: domain.into(),
311 text: text.into(),
312 }
313 }
314
315 pub fn ehlo<D, G>(domain: D, greet: Option<G>, capabilities: Vec<Capability>) -> Response
316 where
317 D: Into<String>,
318 G: Into<String>,
319 {
320 Response::Ehlo {
321 domain: domain.into(),
322 greet: greet.map(Into::into),
323 capabilities,
324 }
325 }
326
327 pub fn other<T>(code: u16, text: T) -> Response
328 where
329 T: Into<String>,
330 {
331 Response::Other {
332 code,
333 text: text.into(),
334 }
335 }
336
337 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
338 match self {
339 Response::Greeting { domain, text } => {
340 let lines = text.lines().collect::<Vec<_>>();
341
342 if let Some((first, tail)) = lines.split_first() {
343 if let Some((last, head)) = tail.split_last() {
344 write!(writer, "220-{} {}\r\n", domain, first)?;
345
346 for line in head {
347 write!(writer, "220-{}\r\n", line)?;
348 }
349
350 write!(writer, "220 {}\r\n", last)?;
351 } else {
352 write!(writer, "220 {} {}\r\n", domain, first)?;
353 }
354 } else {
355 write!(writer, "220 {}\r\n", domain)?;
356 }
357 }
358 Response::Ehlo {
359 domain,
360 greet,
361 capabilities,
362 } => {
363 let greet = match greet {
364 Some(greet) => format!(" {}", greet),
365 None => "".to_string(),
366 };
367
368 if let Some((tail, head)) = capabilities.split_last() {
369 writer.write_all(format!("250-{}{}\r\n", domain, greet).as_bytes())?;
370
371 for capability in head {
372 writer.write_all(b"250-")?;
373 capability.serialize(writer)?;
374 writer.write_all(b"\r\n")?;
375 }
376
377 writer.write_all(b"250 ")?;
378 tail.serialize(writer)?;
379 writer.write_all(b"\r\n")?;
380 } else {
381 writer.write_all(format!("250 {}{}\r\n", domain, greet).as_bytes())?;
382 }
383 }
384 Response::Other { code, text } => {
385 let lines = text.lines().collect::<Vec<_>>();
386
387 if let Some((last, head)) = lines.split_last() {
388 for line in head {
389 write!(writer, "{}-{}\r\n", code, line)?;
390 }
391
392 write!(writer, "{} {}\r\n", code, last)?;
393 } else {
394 write!(writer, "{}\r\n", code)?;
395 }
396 }
397 }
398
399 Ok(())
400 }
401}
402
403#[cfg_attr(feature = "serdex", derive(Serialize, Deserialize))]
406#[derive(Debug, Clone, PartialEq, Eq)]
407pub enum Capability {
408 EXPN,
444 Help,
447
448 EightBitMIME,
450
451 Size(u32),
453
454 Chunking,
456
457 BinaryMIME,
459
460 Checkpoint,
462
463 DeliverBy,
465
466 Pipelining,
468
469 DSN,
471
472 ETRN,
475
476 EnhancedStatusCodes,
478
479 StartTLS,
481
482 MTRK,
487
488 ATRN,
491
492 Auth(Vec<AuthMechanism>),
494
495 BURL,
498
499 SMTPUTF8,
510
511 RRVS,
516
517 RequireTLS,
519
520 Other {
527 keyword: String,
528 params: Vec<String>,
529 },
530}
531
532impl Capability {
533 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
534 match self {
535 Capability::EXPN => writer.write_all(b"EXPN"),
536 Capability::Help => writer.write_all(b"HELP"),
537 Capability::EightBitMIME => writer.write_all(b"8BITMIME"),
538 Capability::Size(number) => writer.write_all(format!("SIZE {}", number).as_bytes()),
539 Capability::Chunking => writer.write_all(b"CHUNKING"),
540 Capability::BinaryMIME => writer.write_all(b"BINARYMIME"),
541 Capability::Checkpoint => writer.write_all(b"CHECKPOINT"),
542 Capability::DeliverBy => writer.write_all(b"DELIVERBY"),
543 Capability::Pipelining => writer.write_all(b"PIPELINING"),
544 Capability::DSN => writer.write_all(b"DSN"),
545 Capability::ETRN => writer.write_all(b"ETRN"),
546 Capability::EnhancedStatusCodes => writer.write_all(b"ENHANCEDSTATUSCODES"),
547 Capability::StartTLS => writer.write_all(b"STARTTLS"),
548 Capability::MTRK => writer.write_all(b"MTRK"),
549 Capability::ATRN => writer.write_all(b"ATRN"),
550 Capability::Auth(mechanisms) => {
551 if let Some((tail, head)) = mechanisms.split_last() {
552 writer.write_all(b"AUTH ")?;
553
554 for mechanism in head {
555 mechanism.serialize(writer)?;
556 writer.write_all(b" ")?;
557 }
558
559 tail.serialize(writer)
560 } else {
561 writer.write_all(b"AUTH")
562 }
563 }
564 Capability::BURL => writer.write_all(b"BURL"),
565 Capability::SMTPUTF8 => writer.write_all(b"SMTPUTF8"),
566 Capability::RRVS => writer.write_all(b"RRVS"),
567 Capability::RequireTLS => writer.write_all(b"REQUIRETLS"),
568 Capability::Other { keyword, params } => {
569 if let Some((tail, head)) = params.split_last() {
570 writer.write_all(keyword.as_bytes())?;
571 writer.write_all(b" ")?;
572
573 for param in head {
574 writer.write_all(param.as_bytes())?;
575 writer.write_all(b" ")?;
576 }
577
578 writer.write_all(tail.as_bytes())
579 } else {
580 writer.write_all(keyword.as_bytes())
581 }
582 }
583 }
584 }
585}
586
587#[cfg_attr(feature = "serdex", derive(Serialize, Deserialize))]
588#[derive(Debug, Clone, PartialEq, Eq)]
589pub enum AuthMechanism {
590 Plain,
591 Login,
592 GSSAPI,
593
594 CramMD5,
595 CramSHA1,
596 ScramMD5,
597 DigestMD5,
598 NTLM,
599
600 Other(String),
601}
602
603impl AuthMechanism {
604 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
605 match self {
606 AuthMechanism::Plain => writer.write_all(b"PLAIN"),
607 AuthMechanism::Login => writer.write_all(b"LOGIN"),
608 AuthMechanism::GSSAPI => writer.write_all(b"GSSAPI"),
609
610 AuthMechanism::CramMD5 => writer.write_all(b"CRAM-MD5"),
611 AuthMechanism::CramSHA1 => writer.write_all(b"CRAM-SHA1"),
612 AuthMechanism::ScramMD5 => writer.write_all(b"SCRAM-MD5"),
613 AuthMechanism::DigestMD5 => writer.write_all(b"DIGEST-MD5"),
614 AuthMechanism::NTLM => writer.write_all(b"NTLM"),
615
616 AuthMechanism::Other(other) => writer.write_all(other.as_bytes()),
617 }
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use crate::types::{Capability, Response};
624
625 #[test]
626 fn test_serialize_greeting() {
627 let tests = &[
628 (
629 Response::Greeting {
630 domain: "example.org".into(),
631 text: "".into(),
632 },
633 b"220 example.org\r\n".as_ref(),
634 ),
635 (
636 Response::Greeting {
637 domain: "example.org".into(),
638 text: "A".into(),
639 },
640 b"220 example.org A\r\n".as_ref(),
641 ),
642 (
643 Response::Greeting {
644 domain: "example.org".into(),
645 text: "A\nB".into(),
646 },
647 b"220-example.org A\r\n220 B\r\n".as_ref(),
648 ),
649 (
650 Response::Greeting {
651 domain: "example.org".into(),
652 text: "A\nB\nC".into(),
653 },
654 b"220-example.org A\r\n220-B\r\n220 C\r\n".as_ref(),
655 ),
656 ];
657
658 for (test, expected) in tests.into_iter() {
659 let mut got = Vec::new();
660 test.serialize(&mut got).unwrap();
661 assert_eq!(expected, &got);
662 }
663 }
664
665 #[test]
666 fn test_serialize_ehlo() {
667 let tests = &[
668 (
669 Response::Ehlo {
670 domain: "example.org".into(),
671 greet: None,
672 capabilities: vec![],
673 },
674 b"250 example.org\r\n".as_ref(),
675 ),
676 (
677 Response::Ehlo {
678 domain: "example.org".into(),
679 greet: Some("...".into()),
680 capabilities: vec![],
681 },
682 b"250 example.org ...\r\n".as_ref(),
683 ),
684 (
685 Response::Ehlo {
686 domain: "example.org".into(),
687 greet: Some("...".into()),
688 capabilities: vec![Capability::StartTLS],
689 },
690 b"250-example.org ...\r\n250 STARTTLS\r\n".as_ref(),
691 ),
692 (
693 Response::Ehlo {
694 domain: "example.org".into(),
695 greet: Some("...".into()),
696 capabilities: vec![Capability::StartTLS, Capability::Size(12345)],
697 },
698 b"250-example.org ...\r\n250-STARTTLS\r\n250 SIZE 12345\r\n".as_ref(),
699 ),
700 ];
701
702 for (test, expected) in tests.into_iter() {
703 let mut got = Vec::new();
704 test.serialize(&mut got).unwrap();
705 assert_eq!(expected, &got);
706 }
707 }
708
709 #[test]
710 fn test_serialize_other() {
711 let tests = &[
712 (
713 Response::Other {
714 code: 333,
715 text: "".into(),
716 },
717 b"333\r\n".as_ref(),
718 ),
719 (
720 Response::Other {
721 code: 333,
722 text: "A".into(),
723 },
724 b"333 A\r\n".as_ref(),
725 ),
726 (
727 Response::Other {
728 code: 333,
729 text: "A\nB".into(),
730 },
731 b"333-A\r\n333 B\r\n".as_ref(),
732 ),
733 ];
734
735 for (test, expected) in tests.into_iter() {
736 let mut got = Vec::new();
737 test.serialize(&mut got).unwrap();
738 assert_eq!(expected, &got);
739 }
740 }
741}