neo_email/
command.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4use tokio::sync::Mutex;
5
6use crate::{
7    client_message::ClientMessage,
8    connection::{SMTPConnection, SMTPConnectionStatus},
9    errors::SMTPError,
10    mail::EmailAddress,
11    message::Message,
12    server::Controllers,
13    status_code::StatusCodes,
14};
15
16/// # SMTP Commands
17///
18/// This enum represents the commands that the SMTP server can receive.
19#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
20#[serde(rename_all = "UPPERCASE")]
21pub enum Commands {
22    /// HELO Command
23    ///
24    /// This command is used to identify the client to the server.
25    HELO,
26    /// Extended HELO
27    ///
28    /// Usually used for getting the server capabilities.
29    EHLO,
30    /// MAIL Command
31    ///
32    /// This command is used to specify the sender of the email.
33    MAIL,
34    /// RCPT Command
35    ///
36    /// This command is used to specify the recipient of the email.
37    RCPT,
38    /// DATA Command
39    /// This command is used to send the email data.
40    DATA,
41    /// RSET Command
42    ///
43    /// This command is used to reset the session.
44    RSET,
45    /// VRFY Command
46    ///
47    /// This command is used to verify the email address.
48    VRFY,
49    /// EXPN Command
50    ///
51    /// This command is used to expand the mailing list.
52    EXPN,
53    /// HELP Command
54    ///
55    /// This command is used to get help from the server.
56    HELP,
57    /// NOOP Command
58    ///
59    /// This command is used to do nothing.
60    NOOP,
61    /// QUIT Command
62    ///
63    /// This command is used to quit the session.
64    QUIT,
65    /// AUTH Command
66    ///
67    /// This command is used to authenticate the user.
68    AUTH,
69    /// STARTTLS Command
70    ///
71    /// This command is used to start the TLS session.
72    STARTTLS,
73    /// Unknown Command
74    ///
75    /// This command is used when the command is not recognized.
76    UNKNOWN(String),
77}
78
79impl Commands {
80    /// # From Bytes
81    ///
82    /// This function converts a byte array to a Commands enum.
83    pub fn from_bytes(bytes: &[u8]) -> Self {
84        // Convert bytes to string, uppercase, trim and convert to string
85        let bytes_to_string = String::from_utf8_lossy(bytes)
86            .to_uppercase()
87            .trim()
88            .to_string();
89        match bytes_to_string.as_str() {
90            "HELO" => Commands::HELO,
91            "EHLO" => Commands::EHLO,
92            "MAIL" => Commands::MAIL,
93            "RCPT" => Commands::RCPT,
94            "DATA" => Commands::DATA,
95            "RSET" => Commands::RSET,
96            "VRFY" => Commands::VRFY,
97            "EXPN" => Commands::EXPN,
98            "HELP" => Commands::HELP,
99            "NOOP" => Commands::NOOP,
100            "QUIT" => Commands::QUIT,
101            "AUTH" => Commands::AUTH,
102            "STARTTLS" => Commands::STARTTLS,
103            _ => Commands::UNKNOWN(bytes_to_string),
104        }
105    }
106
107    /// # Parse MAIL Command Data
108    /// 
109    /// This function parses the data from the MAIL command.
110    pub fn parse_mail_command_data(data: String) -> Result<EmailAddress, SMTPError> {
111        // Trim any leading or trailing whitespace
112        let data = data.trim();
113
114        // Extract the part between '<' and '>'
115        let start = data
116            .find('<')
117            .ok_or(SMTPError::ParseError("Invalid email address".to_string()))?;
118        let end = data
119            .find('>')
120            .ok_or(SMTPError::ParseError("Invalid email address".to_string()))?;
121
122        // Extract and trim the email address part
123        let email_address = &data[start + 1..end];
124        EmailAddress::from_string(email_address)
125            .map_err(|_| SMTPError::ParseError("Invalid email address".to_string()))
126    }
127
128    /// # Parse RCPT Command Data
129    /// 
130    /// This function parses the data from the RCPT command.
131    pub fn parse_rcpt_command_data(data: String) -> Result<EmailAddress, SMTPError> {
132        // Trim any leading or trailing whitespace
133        let data = data.trim();
134
135        // Extract the part between '<' and '>'
136        let start = data
137            .find('<')
138            .ok_or(SMTPError::ParseError("Invalid email address".to_string()))?;
139        let end = data
140            .find('>')
141            .ok_or(SMTPError::ParseError("Invalid email address".to_string()))?;
142
143        // Extract and trim the email address part
144        let email_address = &data[start + 1..end];
145        EmailAddress::from_string(email_address)
146            .map_err(|_| SMTPError::ParseError("Invalid email address".to_string()))
147    }
148}
149
150/// # Handle Command
151/// 
152/// This function handles the SMTP command.
153pub async fn handle_command<B>(
154    conn: Arc<Mutex<SMTPConnection<B>>>,
155    controllers: Controllers<B>,
156    client_message: &mut ClientMessage<String>,
157    allowed_commands: Vec<Commands>,
158    max_size: usize,
159) -> Result<(Vec<Message>, SMTPConnectionStatus), SMTPError>
160where
161    B: 'static + Default + Send + Sync + Clone,
162{
163    log::trace!("[⚙️] Handling SMTP command: {:?}", client_message.command);
164
165    // Check if the command is allowed
166    if allowed_commands
167        .iter()
168        .find(|&cmd| cmd == &client_message.command)
169        .is_none()
170    {
171        return Err(SMTPError::UnknownCommand(client_message.command.clone()));
172    }
173
174    let result = match client_message.command {
175        Commands::HELO => (
176            vec![Message::builder()
177                .status(StatusCodes::OK)
178                .message(format!("Hello {}", "unknown"))
179                .build()],
180            SMTPConnectionStatus::WaitingCommand,
181        ),
182        Commands::EHLO => {
183            let mut ehlo_messages = vec![
184                Message::builder()
185                    .status(StatusCodes::OK)
186                    .message("Hello".to_string())
187                    .build(),
188                Message::builder()
189                    .status(StatusCodes::OK)
190                    .message(format!("SIZE {}", max_size))
191                    .build(),
192                Message::builder()
193                    .status(StatusCodes::OK)
194                    .message("8BITMIME".to_string())
195                    .build(),
196                Message::builder()
197                    .status(StatusCodes::OK)
198                    .message("PIPELINING".to_string())
199                    .build(),
200                Message::builder()
201                    .status(StatusCodes::OK)
202                    .message("HELP".to_string())
203                    .build(),
204            ];
205
206            let conn = conn.lock().await;
207            if !conn.use_tls {
208                ehlo_messages.push(
209                    Message::builder()
210                        .status(StatusCodes::OK)
211                        .message("STARTTLS".to_string())
212                        .build(),
213                )
214            }
215
216            if controllers.on_auth.is_some() {
217                ehlo_messages.push(
218                    Message::builder()
219                        .status(StatusCodes::OK)
220                        .message(
221                            "AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5 GSSAPI NTLM XOAUTH2".to_string(),
222                        )
223                        .build(),
224                );
225            }
226
227            drop(conn);
228
229            (ehlo_messages, SMTPConnectionStatus::WaitingCommand)
230        }
231        Commands::MAIL => {
232            if let Some(on_mail_cmd) = &controllers.on_mail_cmd {
233                let on_mail_cmd = on_mail_cmd.0.clone();
234                match on_mail_cmd(conn.clone(), client_message.data.clone()).await {
235                    Ok(response) => {
236                        return Ok((vec![response], SMTPConnectionStatus::WaitingCommand))
237                    }
238                    Err(response) => return Ok((vec![response], SMTPConnectionStatus::Closed)),
239                }
240            } else {
241                (
242                    vec![Message::builder()
243                        .status(StatusCodes::OK)
244                        .message("Ok".to_string())
245                        .build()],
246                    SMTPConnectionStatus::WaitingCommand,
247                )
248            }
249        }
250        Commands::RCPT => {
251            if let Some(on_rcpt_cmd) = &controllers.on_rcpt_cmd {
252                let on_rcpt_cmd = on_rcpt_cmd.0.clone();
253                match on_rcpt_cmd(conn.clone(), client_message.data.clone()).await {
254                    Ok(response) => (vec![response], SMTPConnectionStatus::WaitingCommand),
255                    Err(response) => (vec![response], SMTPConnectionStatus::Closed),
256                }
257            } else {
258                let last_command = conn.lock().await;
259                let last_command = last_command
260                    .tracing_commands
261                    .last()
262                    .unwrap_or(&Commands::HELO);
263
264                if last_command != &Commands::MAIL && last_command != &Commands::RCPT {
265                    (
266                        vec![Message::builder()
267                            .status(StatusCodes::BadSequenceOfCommands)
268                            .message("Bad sequence of commands".to_string())
269                            .build()],
270                        SMTPConnectionStatus::WaitingCommand,
271                    )
272                } else {
273                    (
274                        vec![Message::builder()
275                            .status(StatusCodes::OK)
276                            .message("Ok".to_string())
277                            .build()],
278                        SMTPConnectionStatus::WaitingCommand,
279                    )
280                }
281            }
282        }
283        Commands::DATA => (
284            vec![Message::builder()
285                .status(StatusCodes::StartMailInput)
286                .message("Start mail input; end with <CRLF>.<CRLF>".to_string())
287                .build()],
288            SMTPConnectionStatus::WaitingData,
289        ),
290        Commands::RSET => (
291            vec![Message::builder()
292                .status(StatusCodes::OK)
293                .message("Hello".to_string())
294                .build()],
295            SMTPConnectionStatus::WaitingCommand,
296        ),
297        Commands::VRFY => (
298            vec![Message::builder()
299                .status(StatusCodes::CannotVerifyUserButWillAcceptMessageAndAttemptDelivery)
300                .message(
301                    "Cannot VRFY user, but will accept message and attempt delivery".to_string(),
302                )
303                .build()],
304            SMTPConnectionStatus::WaitingCommand,
305        ),
306        Commands::EXPN => (
307            vec![Message::builder()
308                .status(StatusCodes::CommandNotImplemented)
309                .message(
310                    "Cannot EXPN user, but will accept message and attempt delivery".to_string(),
311                )
312                .build()],
313            SMTPConnectionStatus::WaitingCommand,
314        ),
315        Commands::HELP => (
316            vec![Message::builder()
317                .status(StatusCodes::HelpMessage)
318                .message("Help message".to_string())
319                .build()],
320            SMTPConnectionStatus::WaitingCommand,
321        ),
322        Commands::NOOP => (
323            vec![Message::builder()
324                .status(StatusCodes::OK)
325                .message("NOOP Command successful".to_string())
326                .build()],
327            SMTPConnectionStatus::WaitingCommand,
328        ),
329        Commands::QUIT => (
330            vec![Message::builder()
331                .status(StatusCodes::ServiceClosingTransmissionChannel)
332                .message("Service closing transmission channel".to_string())
333                .build()],
334            SMTPConnectionStatus::Closed,
335        ),
336        Commands::AUTH => {
337            if let Some(on_auth) = &controllers.on_auth {
338                let on_auth = on_auth.0.clone();
339                match on_auth(conn.clone(), client_message.data.clone()).await {
340                    Ok(response) => (vec![response], SMTPConnectionStatus::WaitingCommand),
341                    Err(response) => return Ok((vec![response], SMTPConnectionStatus::Closed)),
342                }
343            } else {
344                (
345                    vec![Message::builder()
346                        .status(StatusCodes::CommandNotImplemented)
347                        .message("Command not recognized".to_string())
348                        .build()],
349                    SMTPConnectionStatus::WaitingCommand,
350                )
351            }
352        }
353        Commands::STARTTLS => {
354            let conn = conn.lock().await;
355
356            if conn.use_tls {
357                (
358                    vec![Message::builder()
359                        .status(StatusCodes::TransactionFailed)
360                        .message("Already using TLS".to_string())
361                        .build()],
362                    SMTPConnectionStatus::WaitingCommand,
363                )
364            } else {
365                (
366                    vec![Message::builder()
367                        .status(StatusCodes::SMTPServiceReady)
368                        .message("Ready to start TLS".to_string())
369                        .build()],
370                    SMTPConnectionStatus::StartTLS,
371                )
372            }
373        }
374        _ => {
375            if let Some(on_unknown_cmd) = &controllers.on_unknown_cmd {
376                let on_unknown_cmd = on_unknown_cmd.0.clone();
377                match on_unknown_cmd(conn.clone(), client_message.command.clone()).await {
378                    Ok(response) => (vec![response], SMTPConnectionStatus::WaitingCommand),
379                    Err(response) => (vec![response], SMTPConnectionStatus::Closed),
380                }
381            } else {
382                (
383                    vec![Message::builder()
384                        .status(StatusCodes::CommandNotImplemented)
385                        .message("Command not recognized".to_string())
386                        .build()],
387                    SMTPConnectionStatus::WaitingCommand,
388                )
389            }
390        }
391    };
392
393    let mut guarded_conn = conn.lock().await;
394    guarded_conn
395        .tracing_commands
396        .push(client_message.command.clone());
397    drop(guarded_conn);
398
399    Ok(result)
400}