From cf9640069dac6aec0eb8120b84e17e31d8bce958 Mon Sep 17 00:00:00 2001 From: Mauro D Date: Fri, 27 Jan 2023 16:01:50 +0000 Subject: [PATCH] v0.1.0 --- .github/FUNDING.yml | 13 ++++++ .github/workflows/rust.yml | 22 ++++++++++ Cargo.toml | 8 ++++ README.md | 37 ++++++++++++++++- src/lib.rs | 61 +++++++++++++++++++++++++++ src/request/mod.rs | 23 +++++++++++ src/request/parser.rs | 85 +++++++++++++++++++++++++++----------- src/request/receiver.rs | 27 +++++++++++- src/response/generate.rs | 27 +++++++++++- src/response/mod.rs | 23 +++++++++++ src/response/parser.rs | 34 +++++++++++---- 11 files changed, 323 insertions(+), 37 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/rust.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e2b24cc --- /dev/null +++ b/.github/FUNDING.yml @@ -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'] diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..5c2da0c --- /dev/null +++ b/.github/workflows/rust.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 123d82b..52b1b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,13 @@ [package] name = "smtp-proto" +description = "SMTP protocol parser" +authors = [ "Stalwart Labs "] +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" edition = "2021" diff --git a/README.md b/README.md index 7ad7071..fa996a1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,37 @@ # 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. diff --git a/src/lib.rs b/src/lib.rs index d2fbe24..610657a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 . + * + * 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; pub mod request; diff --git a/src/request/mod.rs b/src/request/mod.rs index 3e93bed..714a6d4 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -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 . + * + * 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 receiver; diff --git a/src/request/parser.rs b/src/request/parser.rs index d96eef7..f4f5f0a 100644 --- a/src/request/parser.rs +++ b/src/request/parser.rs @@ -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 . + * + * 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 crate::*; @@ -7,6 +30,9 @@ use super::*; #[derive(Default)] pub struct RequestParser {} +const MAX_ADDRESS_LEN: usize = 256; +const MAX_DOMAIN_LEN: usize = 255; + impl Request { pub fn parse(bytes: &mut Iter<'_, u8>) -> Result, Error> { let mut parser = Rfc5321Parser::new(bytes); @@ -70,7 +96,7 @@ impl Request { if parser.stop_char != LF { let host = parser.text()?; parser.seek_lf()?; - if !host.is_empty() { + if (1..=MAX_DOMAIN_LEN).contains(&host.len()) { return Ok(Request::Ehlo { host }); } } @@ -136,7 +162,7 @@ impl Request { if parser.stop_char != LF { let value = parser.string()?; parser.seek_lf()?; - if !value.is_empty() { + if (1..=MAX_ADDRESS_LEN).contains(&value.len()) { return Ok(Request::Expn { value }); } } @@ -163,7 +189,7 @@ impl Request { if parser.stop_char != LF { let host = parser.text()?; parser.seek_lf()?; - if !host.is_empty() { + if (1..=MAX_DOMAIN_LEN).contains(&host.len()) { return Ok(Request::Lhlo { host }); } } @@ -179,7 +205,7 @@ impl Request { if parser.stop_char != LF { let value = parser.string()?; parser.seek_lf()?; - if !value.is_empty() { + if (1..=MAX_ADDRESS_LEN).contains(&value.len()) { return Ok(Request::Vrfy { value }); } } @@ -272,7 +298,7 @@ impl Request { if parser.stop_char != LF { let host = parser.text()?; parser.seek_lf()?; - if !host.is_empty() { + if (1..=MAX_DOMAIN_LEN).contains(&host.len()) { return Ok(Request::Helo { host }); } } @@ -423,11 +449,13 @@ impl<'x, 'y> Rfc5321Parser<'x, 'y> { self.stop_char = ch; let value = value.into_string(); let len = value.chars().count(); - return Ok(if len == 0 || len < 255 && at_count == 1 && lp_len > 0 { - value.into() - } else { - None - }); + return Ok( + if len == 0 || len <= MAX_ADDRESS_LEN && at_count == 1 && lp_len > 0 { + value.into() + } else { + None + }, + ); } b'\r' => (), b':' if !in_quote && matches!(value.first(), Some(b'@')) => { @@ -442,22 +470,26 @@ impl<'x, 'y> Rfc5321Parser<'x, 'y> { self.stop_char = b' '; let value = value.into_string(); let len = value.chars().count(); - return Ok(if len == 0 || len < 255 && at_count == 1 && lp_len > 0 { - value.into() - } else { - None - }); + return Ok( + if len == 0 || len <= MAX_ADDRESS_LEN && at_count == 1 && lp_len > 0 { + value.into() + } else { + None + }, + ); } } b'\n' => { self.stop_char = b'\n'; let value = value.into_string(); let len = value.chars().count(); - return Ok(if len == 0 || len < 255 && at_count == 1 && lp_len > 0 { - value.into() - } else { - None - }); + return Ok( + if len == 0 || len <= MAX_ADDRESS_LEN && at_count == 1 && lp_len > 0 { + value.into() + } else { + None + }, + ); } b'\"' if !in_quote || last_ch != b'\\' => { in_quote = !in_quote; @@ -937,7 +969,9 @@ impl<'x, 'y> Rfc5321Parser<'x, 'y> { } AUTH_ if self.stop_char == b'=' => { 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(); } else { self.seek_lf()?; @@ -1039,7 +1073,9 @@ impl<'x, 'y> Rfc5321Parser<'x, 'y> { return Err(Error::InvalidParameter { param: "ORCPT" }); } 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(); } else { self.seek_lf()?; @@ -2081,12 +2117,11 @@ mod tests { let (request, parsed_request): (&str, Result, Error>) = item; for extra in ["\n", "\r\n", " \n", " \r\n"] { - let request = format!("{}{}", request, extra); + let request = format!("{request}{extra}"); assert_eq!( parsed_request, Request::parse(&mut request.as_bytes().iter()), - "failed for {:?}", - request + "failed for {request:?}" ); } } diff --git a/src/request/receiver.rs b/src/request/receiver.rs index 7d0234d..7207516 100644 --- a/src/request/receiver.rs +++ b/src/request/receiver.rs @@ -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 . + * + * 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 crate::{Error, Request}; @@ -247,7 +270,7 @@ mod tests { continue 'outer; } } - panic!("Failed for {:?}", data); + panic!("Failed for {data:?}"); } } @@ -295,7 +318,7 @@ mod tests { Err(Error::NeedsMoreData { .. }) => { break; } - err => panic!("Unexpected error {:?}", err), + err => panic!("Unexpected error {err:?}"), } } } diff --git a/src/response/generate.rs b/src/response/generate.rs index 0880c60..cd52e90 100644 --- a/src/response/generate.rs +++ b/src/response/generate.rs @@ -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 . + * + * 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, io::{self, Write}, @@ -39,7 +62,7 @@ impl EhloResponse { while mechanisms != 0 { let item = 1 << (63 - mechanisms.leading_zeros()); mechanisms ^= item; - write!(writer, " {}", (item as u64).to_mechanism())?; + write!(writer, " {}", item.to_mechanism())?; } writer.write_all(b"\r\n") } @@ -79,7 +102,7 @@ impl EhloResponse { EXT_MTRK => write!(writer, "MTRK\r\n"), EXT_NO_SOLICITING => { if let Some(keywords) = &self.no_soliciting { - write!(writer, "NO-SOLICITING {}\r\n", keywords) + write!(writer, "NO-SOLICITING {keywords}\r\n") } else { write!(writer, "NO-SOLICITING\r\n") } diff --git a/src/response/mod.rs b/src/response/mod.rs index b07ad15..e507d22 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -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 . + * + * 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 crate::{EhloResponse, Response}; diff --git a/src/response/parser.rs b/src/response/parser.rs index f9085de..3d80ae6 100644 --- a/src/response/parser.rs +++ b/src/response/parser.rs @@ -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 . + * + * 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 crate::{request::parser::Rfc5321Parser, *}; @@ -480,8 +503,7 @@ mod tests { assert_eq!( parsed_response, EhloResponse::parse(&mut response.as_bytes().iter()), - "failed for {:?}", - response + "failed for {response:?}", ); } } @@ -624,8 +646,7 @@ mod tests { ResponseReceiver::default() .parse(&mut response.as_bytes().iter()) .unwrap(), - "failed for {:?}", - response + "failed for {response:?}", ); all_responses.extend_from_slice(response.as_bytes()); all_parsed_responses.push(parsed_response); @@ -643,15 +664,14 @@ mod tests { assert_eq!( parsed_response.next(), Some(response), - "chunk size {}", - chunk_size + "chunk size {chunk_size}", ); receiver.reset(); } Err(Error::NeedsMoreData { .. }) => { break; } - err => panic!("Unexpected error {:?} for chunk size {}", err, chunk_size), + err => panic!("Unexpected error {err:?} for chunk size {chunk_size}"), } } }