This commit is contained in:
Mauro D 2023-01-27 16:01:50 +00:00
parent 1dbb2ab8fc
commit cf9640069d
11 changed files with 323 additions and 37 deletions

13
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,13 @@
# These are supported funding model platforms
github: stalwartlabs
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

22
.github/workflows/rust.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: "CI"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View file

@ -1,5 +1,13 @@
[package] [package]
name = "smtp-proto" name = "smtp-proto"
description = "SMTP protocol parser"
authors = [ "Stalwart Labs <hello@stalw.art>"]
repository = "https://github.com/stalwartlabs/smtp-proto"
homepage = "https://github.com/stalwartlabs/smtp-proto"
license = "AGPL-3.0-only"
keywords = ["smtp", "lmtp", "protocol", "parser"]
categories = ["email", "parser-implementations"]
readme = "README.md"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View file

@ -1,2 +1,37 @@
# smtp-proto # smtp-proto
SMTP Protocol
[![crates.io](https://img.shields.io/crates/v/smtp-proto)](https://crates.io/crates/smtp-proto)
[![build](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml)
[![docs.rs](https://img.shields.io/docsrs/smtp-proto)](https://docs.rs/smtp-proto)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
_smtp-proto_ is a fast SMTP/LMTP parser for Rust that supports all [registered SMTP service extensions](https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml).
The library is part of Stalwart SMTP and LMTP servers. It is not yet documented so if you need help using the library please start a discussion.
## Testing & Fuzzing
To run the testsuite:
```bash
$ cargo test
```
To fuzz the library with `cargo-fuzz`:
```bash
$ cargo +nightly fuzz run smtp_proto
```
## License
Licensed under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by
the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
See [LICENSE](LICENSE) for more details.
You can be released from the requirements of the AGPLv3 license by purchasing
a commercial license. Please contact licensing@stalw.art for more details.
## Copyright
Copyright (C) 2020-2023, Stalwart Labs Ltd.

View file

@ -1,3 +1,64 @@
/*
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
*
* This file is part of the Stalwart SMTP protocol parser.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
//! # smtp-proto
//!
//! [![crates.io](https://img.shields.io/crates/v/smtp-proto)](https://crates.io/crates/smtp-proto)
//! [![build](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml)
//! [![docs.rs](https://img.shields.io/docsrs/smtp-proto)](https://docs.rs/smtp-proto)
//! [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
//!
//! _smtp-proto_ is a fast SMTP/LMTP parser for Rust that supports all [registered SMTP service extensions](https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml).
//! The library is part of Stalwart SMTP and LMTP servers. It is not yet documented so if you need help using the library please start a discussion.
//!
//!
//! ## Testing & Fuzzing
//!
//! To run the testsuite:
//!
//! ```bash
//! $ cargo test
//! ```
//!
//! To fuzz the library with `cargo-fuzz`:
//!
//! ```bash
//! $ cargo +nightly fuzz run smtp_proto
//! ```
//!
//! ## License
//!
//! Licensed under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by
//! the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//! See [LICENSE](LICENSE) for more details.
//!
//! You can be released from the requirements of the AGPLv3 license by purchasing
//! a commercial license. Please contact licensing@stalw.art for more details.
//!
//! ## Copyright
//!
//! Copyright (C) 2020-2023, Stalwart Labs Ltd.
use std::fmt::Display; use std::fmt::Display;
pub mod request; pub mod request;

View file

@ -1,3 +1,26 @@
/*
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
*
* This file is part of the Stalwart SMTP protocol parser.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
pub mod parser; pub mod parser;
pub mod receiver; pub mod receiver;

View file

@ -1,3 +1,26 @@
/*
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
*
* This file is part of the Stalwart SMTP protocol parser.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::slice::Iter; use std::slice::Iter;
use crate::*; use crate::*;
@ -7,6 +30,9 @@ use super::*;
#[derive(Default)] #[derive(Default)]
pub struct RequestParser {} pub struct RequestParser {}
const MAX_ADDRESS_LEN: usize = 256;
const MAX_DOMAIN_LEN: usize = 255;
impl Request<String> { impl Request<String> {
pub fn parse(bytes: &mut Iter<'_, u8>) -> Result<Request<String>, Error> { pub fn parse(bytes: &mut Iter<'_, u8>) -> Result<Request<String>, Error> {
let mut parser = Rfc5321Parser::new(bytes); let mut parser = Rfc5321Parser::new(bytes);
@ -70,7 +96,7 @@ impl Request<String> {
if parser.stop_char != LF { if parser.stop_char != LF {
let host = parser.text()?; let host = parser.text()?;
parser.seek_lf()?; parser.seek_lf()?;
if !host.is_empty() { if (1..=MAX_DOMAIN_LEN).contains(&host.len()) {
return Ok(Request::Ehlo { host }); return Ok(Request::Ehlo { host });
} }
} }
@ -136,7 +162,7 @@ impl Request<String> {
if parser.stop_char != LF { if parser.stop_char != LF {
let value = parser.string()?; let value = parser.string()?;
parser.seek_lf()?; parser.seek_lf()?;
if !value.is_empty() { if (1..=MAX_ADDRESS_LEN).contains(&value.len()) {
return Ok(Request::Expn { value }); return Ok(Request::Expn { value });
} }
} }
@ -163,7 +189,7 @@ impl Request<String> {
if parser.stop_char != LF { if parser.stop_char != LF {
let host = parser.text()?; let host = parser.text()?;
parser.seek_lf()?; parser.seek_lf()?;
if !host.is_empty() { if (1..=MAX_DOMAIN_LEN).contains(&host.len()) {
return Ok(Request::Lhlo { host }); return Ok(Request::Lhlo { host });
} }
} }
@ -179,7 +205,7 @@ impl Request<String> {
if parser.stop_char != LF { if parser.stop_char != LF {
let value = parser.string()?; let value = parser.string()?;
parser.seek_lf()?; parser.seek_lf()?;
if !value.is_empty() { if (1..=MAX_ADDRESS_LEN).contains(&value.len()) {
return Ok(Request::Vrfy { value }); return Ok(Request::Vrfy { value });
} }
} }
@ -272,7 +298,7 @@ impl Request<String> {
if parser.stop_char != LF { if parser.stop_char != LF {
let host = parser.text()?; let host = parser.text()?;
parser.seek_lf()?; parser.seek_lf()?;
if !host.is_empty() { if (1..=MAX_DOMAIN_LEN).contains(&host.len()) {
return Ok(Request::Helo { host }); return Ok(Request::Helo { host });
} }
} }
@ -423,11 +449,13 @@ impl<'x, 'y> Rfc5321Parser<'x, 'y> {
self.stop_char = ch; self.stop_char = ch;
let value = value.into_string(); let value = value.into_string();
let len = value.chars().count(); let len = value.chars().count();
return Ok(if len == 0 || len < 255 && at_count == 1 && lp_len > 0 { return Ok(
if len == 0 || len <= MAX_ADDRESS_LEN && at_count == 1 && lp_len > 0 {
value.into() value.into()
} else { } else {
None None
}); },
);
} }
b'\r' => (), b'\r' => (),
b':' if !in_quote && matches!(value.first(), Some(b'@')) => { b':' if !in_quote && matches!(value.first(), Some(b'@')) => {
@ -442,22 +470,26 @@ impl<'x, 'y> Rfc5321Parser<'x, 'y> {
self.stop_char = b' '; self.stop_char = b' ';
let value = value.into_string(); let value = value.into_string();
let len = value.chars().count(); let len = value.chars().count();
return Ok(if len == 0 || len < 255 && at_count == 1 && lp_len > 0 { return Ok(
if len == 0 || len <= MAX_ADDRESS_LEN && at_count == 1 && lp_len > 0 {
value.into() value.into()
} else { } else {
None None
}); },
);
} }
} }
b'\n' => { b'\n' => {
self.stop_char = b'\n'; self.stop_char = b'\n';
let value = value.into_string(); let value = value.into_string();
let len = value.chars().count(); let len = value.chars().count();
return Ok(if len == 0 || len < 255 && at_count == 1 && lp_len > 0 { return Ok(
if len == 0 || len <= MAX_ADDRESS_LEN && at_count == 1 && lp_len > 0 {
value.into() value.into()
} else { } else {
None None
}); },
);
} }
b'\"' if !in_quote || last_ch != b'\\' => { b'\"' if !in_quote || last_ch != b'\\' => {
in_quote = !in_quote; in_quote = !in_quote;
@ -937,7 +969,9 @@ impl<'x, 'y> Rfc5321Parser<'x, 'y> {
} }
AUTH_ if self.stop_char == b'=' => { AUTH_ if self.stop_char == b'=' => {
let mailbox = self.xtext()?; let mailbox = self.xtext()?;
if !mailbox.is_empty() && self.stop_char.is_ascii_whitespace() { if (1..=MAX_ADDRESS_LEN).contains(&mailbox.len())
&& self.stop_char.is_ascii_whitespace()
{
params.auth = mailbox.into(); params.auth = mailbox.into();
} else { } else {
self.seek_lf()?; self.seek_lf()?;
@ -1039,7 +1073,9 @@ impl<'x, 'y> Rfc5321Parser<'x, 'y> {
return Err(Error::InvalidParameter { param: "ORCPT" }); return Err(Error::InvalidParameter { param: "ORCPT" });
} }
let addr = self.xtext()?; let addr = self.xtext()?;
if self.stop_char.is_ascii_whitespace() && (1..=500).contains(&addr.len()) { if self.stop_char.is_ascii_whitespace()
&& (1..=MAX_ADDRESS_LEN).contains(&addr.len())
{
params.orcpt = addr.into(); params.orcpt = addr.into();
} else { } else {
self.seek_lf()?; self.seek_lf()?;
@ -2081,12 +2117,11 @@ mod tests {
let (request, parsed_request): (&str, Result<Request<String>, Error>) = item; let (request, parsed_request): (&str, Result<Request<String>, Error>) = item;
for extra in ["\n", "\r\n", " \n", " \r\n"] { for extra in ["\n", "\r\n", " \n", " \r\n"] {
let request = format!("{}{}", request, extra); let request = format!("{request}{extra}");
assert_eq!( assert_eq!(
parsed_request, parsed_request,
Request::parse(&mut request.as_bytes().iter()), Request::parse(&mut request.as_bytes().iter()),
"failed for {:?}", "failed for {request:?}"
request
); );
} }
} }

View file

@ -1,3 +1,26 @@
/*
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
*
* This file is part of the Stalwart SMTP protocol parser.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::slice::Iter; use std::slice::Iter;
use crate::{Error, Request}; use crate::{Error, Request};
@ -247,7 +270,7 @@ mod tests {
continue 'outer; continue 'outer;
} }
} }
panic!("Failed for {:?}", data); panic!("Failed for {data:?}");
} }
} }
@ -295,7 +318,7 @@ mod tests {
Err(Error::NeedsMoreData { .. }) => { Err(Error::NeedsMoreData { .. }) => {
break; break;
} }
err => panic!("Unexpected error {:?}", err), err => panic!("Unexpected error {err:?}"),
} }
} }
} }

View file

@ -1,3 +1,26 @@
/*
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
*
* This file is part of the Stalwart SMTP protocol parser.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::{ use std::{
fmt::Display, fmt::Display,
io::{self, Write}, io::{self, Write},
@ -39,7 +62,7 @@ impl<T: Display> EhloResponse<T> {
while mechanisms != 0 { while mechanisms != 0 {
let item = 1 << (63 - mechanisms.leading_zeros()); let item = 1 << (63 - mechanisms.leading_zeros());
mechanisms ^= item; mechanisms ^= item;
write!(writer, " {}", (item as u64).to_mechanism())?; write!(writer, " {}", item.to_mechanism())?;
} }
writer.write_all(b"\r\n") writer.write_all(b"\r\n")
} }
@ -79,7 +102,7 @@ impl<T: Display> EhloResponse<T> {
EXT_MTRK => write!(writer, "MTRK\r\n"), EXT_MTRK => write!(writer, "MTRK\r\n"),
EXT_NO_SOLICITING => { EXT_NO_SOLICITING => {
if let Some(keywords) = &self.no_soliciting { if let Some(keywords) = &self.no_soliciting {
write!(writer, "NO-SOLICITING {}\r\n", keywords) write!(writer, "NO-SOLICITING {keywords}\r\n")
} else { } else {
write!(writer, "NO-SOLICITING\r\n") write!(writer, "NO-SOLICITING\r\n")
} }

View file

@ -1,3 +1,26 @@
/*
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
*
* This file is part of the Stalwart SMTP protocol parser.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::fmt::Display; use std::fmt::Display;
use crate::{EhloResponse, Response}; use crate::{EhloResponse, Response};

View file

@ -1,3 +1,26 @@
/*
* Copyright (c) 2020-2023, Stalwart Labs Ltd.
*
* This file is part of the Stalwart SMTP protocol parser.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::slice::Iter; use std::slice::Iter;
use crate::{request::parser::Rfc5321Parser, *}; use crate::{request::parser::Rfc5321Parser, *};
@ -480,8 +503,7 @@ mod tests {
assert_eq!( assert_eq!(
parsed_response, parsed_response,
EhloResponse::parse(&mut response.as_bytes().iter()), EhloResponse::parse(&mut response.as_bytes().iter()),
"failed for {:?}", "failed for {response:?}",
response
); );
} }
} }
@ -624,8 +646,7 @@ mod tests {
ResponseReceiver::default() ResponseReceiver::default()
.parse(&mut response.as_bytes().iter()) .parse(&mut response.as_bytes().iter())
.unwrap(), .unwrap(),
"failed for {:?}", "failed for {response:?}",
response
); );
all_responses.extend_from_slice(response.as_bytes()); all_responses.extend_from_slice(response.as_bytes());
all_parsed_responses.push(parsed_response); all_parsed_responses.push(parsed_response);
@ -643,15 +664,14 @@ mod tests {
assert_eq!( assert_eq!(
parsed_response.next(), parsed_response.next(),
Some(response), Some(response),
"chunk size {}", "chunk size {chunk_size}",
chunk_size
); );
receiver.reset(); receiver.reset();
} }
Err(Error::NeedsMoreData { .. }) => { Err(Error::NeedsMoreData { .. }) => {
break; break;
} }
err => panic!("Unexpected error {:?} for chunk size {}", err, chunk_size), err => panic!("Unexpected error {err:?} for chunk size {chunk_size}"),
} }
} }
} }