1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7use use_email_address::AddressValidationError;
8use use_email_envelope::{MailFromPath, RcptToPath};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12pub enum SmtpError {
13 Address(AddressValidationError),
15 InvalidReplyCode,
17 Empty,
19 InvalidEnhancedStatus,
21}
22
23impl fmt::Display for SmtpError {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::Address(error) => write!(formatter, "{error}"),
27 Self::InvalidReplyCode => {
28 formatter.write_str("SMTP reply code must be between 100 and 599")
29 }
30 Self::Empty => formatter.write_str("SMTP value cannot be empty"),
31 Self::InvalidEnhancedStatus => formatter.write_str("invalid SMTP enhanced status code"),
32 }
33 }
34}
35
36impl Error for SmtpError {
37 fn source(&self) -> Option<&(dyn Error + 'static)> {
38 match self {
39 Self::Address(error) => Some(error),
40 Self::InvalidReplyCode | Self::Empty | Self::InvalidEnhancedStatus => None,
41 }
42 }
43}
44
45impl From<AddressValidationError> for SmtpError {
46 fn from(value: AddressValidationError) -> Self {
47 Self::Address(value)
48 }
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
53pub enum SmtpCommand {
54 Helo(String),
56 Ehlo(String),
58 MailFrom(MailFrom),
60 RcptTo(RcptTo),
62 Data(DataCommand),
64 Quit(QuitCommand),
66 StartTls,
68}
69
70impl fmt::Display for SmtpCommand {
71 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 Self::Helo(domain) => write!(formatter, "HELO {domain}"),
74 Self::Ehlo(domain) => write!(formatter, "EHLO {domain}"),
75 Self::MailFrom(command) => write!(formatter, "{command}"),
76 Self::RcptTo(command) => write!(formatter, "{command}"),
77 Self::Data(command) => write!(formatter, "{command}"),
78 Self::Quit(command) => write!(formatter, "{command}"),
79 Self::StartTls => formatter.write_str("STARTTLS"),
80 }
81 }
82}
83
84#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub struct SmtpReplyCode(u16);
87
88impl SmtpReplyCode {
89 pub const fn new(value: u16) -> Result<Self, SmtpError> {
91 if value >= 100 && value <= 599 {
92 Ok(Self(value))
93 } else {
94 Err(SmtpError::InvalidReplyCode)
95 }
96 }
97
98 #[must_use]
100 pub const fn value(self) -> u16 {
101 self.0
102 }
103}
104
105impl fmt::Display for SmtpReplyCode {
106 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107 write!(formatter, "{}", self.0)
108 }
109}
110
111#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
113pub struct SmtpEnhancedStatusCode {
114 class: u8,
115 subject: u16,
116 detail: u16,
117}
118
119impl SmtpEnhancedStatusCode {
120 pub const fn new(class: u8, subject: u16, detail: u16) -> Result<Self, SmtpError> {
122 if matches!(class, 2 | 4 | 5) {
123 Ok(Self {
124 class,
125 subject,
126 detail,
127 })
128 } else {
129 Err(SmtpError::InvalidEnhancedStatus)
130 }
131 }
132}
133
134impl fmt::Display for SmtpEnhancedStatusCode {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(formatter, "{}.{}.{}", self.class, self.subject, self.detail)
137 }
138}
139
140#[derive(Clone, Debug, Eq, PartialEq)]
142pub struct SmtpReply {
143 code: SmtpReplyCode,
144 enhanced_status: Option<SmtpEnhancedStatusCode>,
145 text: String,
146}
147
148impl SmtpReply {
149 pub fn new(code: SmtpReplyCode, text: impl AsRef<str>) -> Result<Self, SmtpError> {
151 Self::with_enhanced_status(code, None, text)
152 }
153
154 pub fn with_enhanced_status(
156 code: SmtpReplyCode,
157 enhanced_status: Option<SmtpEnhancedStatusCode>,
158 text: impl AsRef<str>,
159 ) -> Result<Self, SmtpError> {
160 let text = validate_text(text.as_ref())?;
161 Ok(Self {
162 code,
163 enhanced_status,
164 text: text.to_owned(),
165 })
166 }
167
168 #[must_use]
170 pub const fn code(&self) -> SmtpReplyCode {
171 self.code
172 }
173}
174
175impl fmt::Display for SmtpReply {
176 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
177 if let Some(status) = self.enhanced_status {
178 write!(formatter, "{} {} {}", self.code, status, self.text)
179 } else {
180 write!(formatter, "{} {}", self.code, self.text)
181 }
182 }
183}
184
185#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
187pub enum SmtpExtension {
188 StartTls,
190 EightBitMime,
192 SmtpUtf8,
194 Size(Option<u64>),
196 Auth(String),
198 Other(String),
200}
201
202impl fmt::Display for SmtpExtension {
203 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
204 match self {
205 Self::StartTls => formatter.write_str("STARTTLS"),
206 Self::EightBitMime => formatter.write_str("8BITMIME"),
207 Self::SmtpUtf8 => formatter.write_str("SMTPUTF8"),
208 Self::Size(Some(limit)) => write!(formatter, "SIZE {limit}"),
209 Self::Size(None) => formatter.write_str("SIZE"),
210 Self::Auth(mechanisms) => write!(formatter, "AUTH {mechanisms}"),
211 Self::Other(value) => formatter.write_str(value),
212 }
213 }
214}
215
216#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
218pub struct EhloKeyword(String);
219
220impl EhloKeyword {
221 pub fn new(value: impl AsRef<str>) -> Result<Self, SmtpError> {
223 validate_text(value.as_ref()).map(|value| Self(value.to_ascii_uppercase()))
224 }
225
226 #[must_use]
228 pub fn as_str(&self) -> &str {
229 &self.0
230 }
231}
232
233impl fmt::Display for EhloKeyword {
234 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
235 formatter.write_str(self.as_str())
236 }
237}
238
239#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
241pub struct MailFrom(MailFromPath);
242
243impl MailFrom {
244 pub fn new(value: impl AsRef<str>) -> Result<Self, SmtpError> {
246 Ok(Self(MailFromPath::new(value)?))
247 }
248
249 #[must_use]
251 pub const fn null() -> Self {
252 Self(MailFromPath::null())
253 }
254
255 #[must_use]
257 pub const fn path(&self) -> &MailFromPath {
258 &self.0
259 }
260}
261
262impl fmt::Display for MailFrom {
263 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
264 write!(formatter, "MAIL FROM:{}", self.0)
265 }
266}
267
268#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
270pub struct RcptTo(RcptToPath);
271
272impl RcptTo {
273 pub fn new(value: impl AsRef<str>) -> Result<Self, SmtpError> {
275 Ok(Self(RcptToPath::new(value)?))
276 }
277
278 #[must_use]
280 pub const fn path(&self) -> &RcptToPath {
281 &self.0
282 }
283}
284
285impl fmt::Display for RcptTo {
286 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
287 write!(formatter, "RCPT TO:{}", self.0)
288 }
289}
290
291#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
293pub struct DataCommand;
294
295impl fmt::Display for DataCommand {
296 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
297 formatter.write_str("DATA")
298 }
299}
300
301#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
303pub struct QuitCommand;
304
305impl fmt::Display for QuitCommand {
306 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
307 formatter.write_str("QUIT")
308 }
309}
310
311#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
313pub struct StartTlsCapability;
314
315#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
317pub struct EightBitMimeCapability;
318
319#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
321pub struct SmtpUtf8Capability;
322
323fn validate_text(value: &str) -> Result<&str, SmtpError> {
324 let trimmed = value.trim();
325 if trimmed.is_empty() {
326 return Err(SmtpError::Empty);
327 }
328 if trimmed
329 .chars()
330 .any(|character| matches!(character, '\r' | '\n'))
331 {
332 return Err(SmtpError::Empty);
333 }
334 Ok(trimmed)
335}
336
337#[cfg(test)]
338mod tests {
339 use super::{MailFrom, RcptTo, SmtpCommand, SmtpEnhancedStatusCode, SmtpReply, SmtpReplyCode};
340
341 #[test]
342 fn renders_commands_and_replies() -> Result<(), Box<dyn std::error::Error>> {
343 let mail_from = SmtpCommand::MailFrom(MailFrom::new("bounce@example.com")?);
344 let rcpt_to = SmtpCommand::RcptTo(RcptTo::new("jane@example.com")?);
345 let reply = SmtpReply::new(SmtpReplyCode::new(250)?, "OK")?;
346 let enhanced = SmtpReply::with_enhanced_status(
347 SmtpReplyCode::new(250)?,
348 Some(SmtpEnhancedStatusCode::new(2, 0, 0)?),
349 "OK",
350 )?;
351
352 assert_eq!(mail_from.to_string(), "MAIL FROM:<bounce@example.com>");
353 assert_eq!(rcpt_to.to_string(), "RCPT TO:<jane@example.com>");
354 assert_eq!(reply.to_string(), "250 OK");
355 assert_eq!(enhanced.to_string(), "250 2.0.0 OK");
356 Ok(())
357 }
358}