Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/smtp/cmd.rs

10.2 KiB, 96 runs

created by r1870400018:593, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use crate::{
2 email::msg::EmailMessage,
3 smtp::codes::SmtpResponseCode,
4};
5
6use oxedyne_fe2o3_core::prelude::*;
7
8use std::{
9 fmt,
10};
11
12
13#[derive(Clone, Debug, PartialEq)]
14pub enum SmtpCommand {
15 /// HELO argument is an unvalidated string -- per RFC 5321 §4.1.1.1
16 /// the client identifier may be either an FQDN *or* an
17 /// address-literal like `[192.168.1.4]` or `[IPv6:::1]`, and even
18 /// then a server SHOULD NOT reject the connection on the basis of
19 /// the identifier failing to verify.
20 Helo(String),
21 /// EHLO argument: same rules as `Helo`.
22 Ehlo(String),
23 MailFrom(String),
24 RcptTo(String),
25 Data,
26 Quit,
27 Rset,
28 Vrfy(String),
29 Expn(String),
30 Help(Option<String>),
31 Noop,
32 Auth(String),
33 StartTls,
34 Email(EmailMessage),
35 Response(SmtpResponseCode, String),
36}
37
38impl fmt::Display for SmtpCommand {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 Self::Helo(domain) => write!(f, "HELO {}", domain),
42 Self::Ehlo(domain) => write!(f, "EHLO {}", domain),
43 Self::MailFrom(address) => write!(f, "MAIL FROM:<{}>", address),
44 Self::RcptTo(address) => write!(f, "RCPT TO:<{}>", address),
45 Self::Data => write!(f, "DATA"),
46 Self::Quit => write!(f, "QUIT"),
47 Self::Rset => write!(f, "RSET"),
48 Self::Vrfy(address) => write!(f, "VRFY {}", address),
49 Self::Expn(address) => write!(f, "EXPN {}", address),
50 Self::Help(Some(topic)) => write!(f, "HELP {}", topic),
51 Self::Help(None) => write!(f, "HELP"),
52 Self::Noop => write!(f, "NOOP"),
53 Self::Auth(mechanism) => write!(f, "AUTH {}", mechanism),
54 Self::StartTls => write!(f, "STARTTLS"),
55 Self::Email(email) => write!(f, "{}", email),
56 Self::Response(code, msg) => write!(f, "{} {}", code, msg),
57 }
58 }
59}
60
61impl FromStr for SmtpCommand {
62 type Err = Error<ErrTag>;
63
64 fn from_str(s: &str) -> Result<Self, Self::Err> {
65 let parts: Vec<&str> = s.split_whitespace().collect();
66 if parts.is_empty() {
67 return Err(err!("Message is empty."; Invalid, Input, Missing));
68 }
69
70 if let Ok(code) = SmtpResponseCode::from_str(parts[0]) {
71 let message = parts[1..].join(" ");
72 return Ok(SmtpCommand::Response(code, message));
73 }
74
75 let cmd = parts[0].to_uppercase();
76
77 match cmd.as_str() {
78 "HELO" => {
79 let rest = s[4..].trim();
80 let domain = rest.split_whitespace().next().unwrap_or("");
81 if domain.is_empty() {
82 Err(err!(
83 "'{}' invalid: HELO command requires a domain.", s;
84 Invalid, Input, Mismatch))
85 } else {
86 Ok(Self::Helo(domain.to_string()))
87 }
88 }
89 "EHLO" => {
90 // RFC 5321 §4.1.1.1: argument is a domain *or* an
91 // address-literal `[IPv4]` / `[IPv6:...]`. Per
92 // §4.1.4 the server SHOULD NOT refuse based on the
93 // identifier failing to verify, so we accept any
94 // non-empty token as-is.
95 let rest = s[4..].trim();
96 let domain = rest.split_whitespace().next().unwrap_or("");
97 if domain.is_empty() {
98 Err(err!(
99 "'{}' invalid: EHLO command requires a domain.", s;
100 Invalid, Input, Mismatch))
101 } else {
102 Ok(Self::Ehlo(domain.to_string()))
103 }
104 }
105 "MAIL" => {
106 // RFC 5321 §4.1.1.2: MAIL FROM:<reverse-path> [SP <esmtp-params>]
107 // Accept whitespace between FROM: and the angle-bracketed
108 // address, and silently ignore any ESMTP extension
109 // parameters that follow the closing '>'.
110 let after = res!(strip_verb(s, "MAIL"));
111 let after_upper = after.to_uppercase();
112 if !after_upper.starts_with("FROM:") {
113 return Err(err!(
114 "'{}' invalid: MAIL command requires 'FROM:'.", s;
115 Invalid, Input));
116 }
117 let after_colon = after[5..].trim_start();
118 let addr = res!(parse_path(after_colon));
119 Ok(SmtpCommand::MailFrom(addr))
120 }
121 "RCPT" => {
122 // RFC 5321 §4.1.1.3: RCPT TO:<forward-path> [SP <esmtp-params>]
123 let after = res!(strip_verb(s, "RCPT"));
124 let after_upper = after.to_uppercase();
125 if !after_upper.starts_with("TO:") {
126 return Err(err!(
127 "'{}' invalid: RCPT command requires 'TO:'.", s;
128 Invalid, Input));
129 }
130 let after_colon = after[3..].trim_start();
131 let addr = res!(parse_path(after_colon));
132 Ok(SmtpCommand::RcptTo(addr))
133 }
134 "DATA" => {
135 if parts.len() != 1 {
136 Err(err!(
137 "'{}' invalid: {} command should have no additional arguments.", s, cmd;
138 Invalid, Input, Excessive))
139 } else {
140 Ok(SmtpCommand::Data)
141 }
142 }
143 "QUIT" => {
144 if parts.len() != 1 {
145 Err(err!(
146 "'{}' invalid: {} command should have no additional arguments.", s, cmd;
147 Invalid, Input, Excessive))
148 } else {
149 Ok(SmtpCommand::Quit)
150 }
151 }
152 "RSET" => {
153 if parts.len() != 1 {
154 Err(err!(
155 "'{}' invalid: {} command should have no additional arguments.", s, cmd;
156 Invalid, Input, Excessive))
157 } else {
158 Ok(SmtpCommand::Rset)
159 }
160 }
161 "VRFY" => {
162 if parts.len() != 2 {
163 Err(err!(
164 "'{}' invalid: {} command requires a single argument.", s, cmd;
165 Invalid, Input, Mismatch))
166 } else {
167 let address = parts[1].trim().to_string();
168 Ok(SmtpCommand::Vrfy(address))
169 }
170 }
171 "EXPN" => {
172 if parts.len() != 2 {
173 Err(err!(
174 "'{}' invalid: {} command requires a single argument.", s, cmd;
175 Invalid, Input, Mismatch))
176 } else {
177 let address = parts[1].trim().to_string();
178 Ok(SmtpCommand::Expn(address))
179 }
180 }
181 "HELP" => {
182 if parts.len() == 1 {
183 Ok(SmtpCommand::Help(None))
184 } else if parts.len() == 2 {
185 let topic = parts[1].trim().to_string();
186 Ok(SmtpCommand::Help(Some(topic)))
187 } else {
188 Err(err!(
189 "'{}' invalid: {} command allows at most one argument.", s, cmd;
190 Invalid, Input, Excessive))
191 }
192 }
193 "NOOP" => {
194 if parts.len() != 1 {
195 Err(err!(
196 "'{}' invalid: {} command should have no additional arguments.", s, cmd;
197 Invalid, Input, Excessive))
198 } else {
199 Ok(SmtpCommand::Noop)
200 }
201 }
202 "AUTH" => {
203 // AUTH takes a mechanism name plus an optional inline
204 // initial response, e.g. `AUTH PLAIN <base64>`. Pass
205 // everything after the command verb through verbatim
206 // so the session loop can split mech and payload.
207 let rest = s[4..].trim_start();
208 if rest.is_empty() {
209 Err(err!(
210 "'{}' invalid: AUTH command requires a mechanism.", s;
211 Invalid, Input, Mismatch))
212 } else {
213 Ok(SmtpCommand::Auth(rest.to_string()))
214 }
215 }
216 "STARTTLS" => {
217 if parts.len() != 1 {
218 Err(err!(
219 "'{}' invalid: {} command should have no additional arguments.", s, cmd;
220 Invalid, Input, Excessive))
221 } else {
222 Ok(SmtpCommand::StartTls)
223 }
224 }
225 _ => Err(err!("Unrecognised command in '{}'.", s; Unknown, Input)),
226 }
227 }
228}
229
230
231/// Strip the leading verb (`MAIL`, `RCPT`, ...) from a command line and
232/// return the remainder, trimmed of leading whitespace. Used by the
233/// `MAIL FROM:` and `RCPT TO:` parsers, which need the raw remainder
234/// rather than a whitespace-split view.
235fn strip_verb(line: &str, verb: &str) -> Outcome<String> {
236 if line.len() < verb.len() {
237 return Err(err!(
238 "Line '{}' shorter than verb '{}'.", line, verb;
239 Invalid, Input));
240 }
241 let head = &line[..verb.len()];
242 if !head.eq_ignore_ascii_case(verb) {
243 return Err(err!(
244 "Line '{}' does not start with verb '{}'.", line, verb;
245 Invalid, Input));
246 }
247 Ok(line[verb.len()..].trim_start().to_string())
248}
249
250/// Parse an SMTP `<reverse-path>` or `<forward-path>` plus optional
251/// trailing ESMTP parameters. Accepts both `<addr>` and the empty
252/// path `<>`. Anything after the closing '>' is silently ignored
253/// (this is where SIZE=, BODY=, ORCPT=, NOTIFY= etc. live).
254fn parse_path(s: &str) -> Outcome<String> {
255 let s = s.trim_start();
256 if !s.starts_with('<') {
257 return Err(err!(
258 "Path '{}' must begin with '<'.", s;
259 Invalid, Input));
260 }
261 let end = match s.find('>') {
262 Some(i) => i,
263 None => return Err(err!(
264 "Path '{}' has no closing '>'.", s;
265 Invalid, Input)),
266 };
267 Ok(s[1..end].to_string())
268}