pax_global_header00006660000000000000000000000064146253556110014522gustar00rootroot0000000000000052 comment=f4d727091591bd0689a06c35913abe12b461839c smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/000077500000000000000000000000001462535561100203515ustar00rootroot00000000000000smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/.github/000077500000000000000000000000001462535561100217115ustar00rootroot00000000000000smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/.github/workflows/000077500000000000000000000000001462535561100237465ustar00rootroot00000000000000smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/.github/workflows/rust.yml000066400000000000000000000004701462535561100254670ustar00rootroot00000000000000name: "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 smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/.gitignore000066400000000000000000000005001462535561100223340ustar00rootroot00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/Cargo.toml000066400000000000000000000010701462535561100222770ustar00rootroot00000000000000[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 = "Apache-2.0 OR MIT" keywords = ["smtp", "lmtp", "protocol", "parser"] categories = ["email", "parser-implementations"] readme = "README.md" version = "0.1.5" edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"], optional = true } [dev-dependencies] [features] default = [] serde_support = ["serde"] smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/LICENSE-APACHE000066400000000000000000000251371462535561100223050ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/LICENSE-MIT000066400000000000000000000017771462535561100220210ustar00rootroot00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/README.md000066400000000000000000000025411462535561100216320ustar00rootroot00000000000000# 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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: Apache](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.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 either of * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ## Copyright Copyright (C) 2020-2024, Stalwart Labs Ltd. smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/fuzz/000077500000000000000000000000001462535561100213475ustar00rootroot00000000000000smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/fuzz/.gitignore000066400000000000000000000000301462535561100233300ustar00rootroot00000000000000target corpus artifacts smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/fuzz/Cargo.toml000066400000000000000000000006261462535561100233030ustar00rootroot00000000000000[package] name = "smtp-proto-fuzz" version = "0.0.0" authors = ["Automatically generated"] publish = false edition = "2018" [package.metadata] cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" [dependencies.smtp-proto] path = ".." # Prevent this from interfering with workspaces [workspace] members = ["."] [[bin]] name = "smtp_proto" path = "fuzz_targets/smtp_proto.rs" test = false doc = false smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/fuzz/fuzz_targets/000077500000000000000000000000001462535561100240765ustar00rootroot00000000000000smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/fuzz/fuzz_targets/smtp_proto.rs000066400000000000000000000057341462535561100266630ustar00rootroot00000000000000/* * Copyright (c) 2020-2024, Stalwart Labs Ltd. * * Licensed under the Apache License, Version 2.0 or the MIT license * , at your * option. This file may not be copied, modified, or distributed * except according to those terms. */ #![no_main] use libfuzzer_sys::fuzz_target; use smtp_proto::{ request::{ parser::Rfc5321Parser, receiver::{ BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver, }, }, response::parser::ResponseReceiver, EhloResponse, Request, }; static RFC5321_ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz:=-<>,; \r\n"; fuzz_target!(|data: &[u8]| { let data_rfc5321 = into_alphabet(data, RFC5321_ALPHABET); for bytes in [data, &data_rfc5321] { let _ = Request::parse(&mut bytes.iter()); let _ = RequestReceiver::default().ingest(&mut bytes.iter(), &[]); let _ = DataReceiver::new().ingest(&mut bytes.iter(), &mut vec![]); let _ = BdatReceiver::new(bytes.len(), true).ingest(&mut bytes.iter(), &mut vec![]); let _ = BdatReceiver::new(bytes.len(), false).ingest(&mut bytes.iter(), &mut vec![]); let _ = DummyDataReceiver::new_bdat(bytes.len()).ingest(&mut bytes.iter()); let _ = DummyDataReceiver::new_data(&DataReceiver::new()).ingest(&mut bytes.iter()); let _ = LineReceiver::new(()).ingest(&mut bytes.iter()); let _ = DummyLineReceiver::default().ingest(&mut bytes.iter()); let _ = ResponseReceiver::default().parse(&mut bytes.iter()); let _ = EhloResponse::::parse(&mut bytes.iter()); let _ = Rfc5321Parser::new(&mut bytes.iter()).hashed_value(); let _ = Rfc5321Parser::new(&mut bytes.iter()).hashed_value_long(); let _ = Rfc5321Parser::new(&mut bytes.iter()).address(); let _ = Rfc5321Parser::new(&mut bytes.iter()).string(); let _ = Rfc5321Parser::new(&mut bytes.iter()).text(); let _ = Rfc5321Parser::new(&mut bytes.iter()).xtext(); let _ = Rfc5321Parser::new(&mut bytes.iter()).seek_char(0); let _ = Rfc5321Parser::new(&mut bytes.iter()).seek_lf(); let _ = Rfc5321Parser::new(&mut bytes.iter()).next_char(); let _ = Rfc5321Parser::new(&mut bytes.iter()).read_char(); let _ = Rfc5321Parser::new(&mut bytes.iter()).size(); let _ = Rfc5321Parser::new(&mut bytes.iter()).integer(); let _ = Rfc5321Parser::new(&mut bytes.iter()).timestamp(); let _ = Rfc5321Parser::new(&mut bytes.iter()).mail_from_parameters(String::new()); let _ = Rfc5321Parser::new(&mut bytes.iter()).rcpt_to_parameters(String::new()); let _ = Rfc5321Parser::new(&mut bytes.iter()).mechanism(); } }); fn into_alphabet(data: &[u8], alphabet: &[u8]) -> Vec { data.iter() .map(|&byte| alphabet[byte as usize % alphabet.len()]) .collect() } smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/000077500000000000000000000000001462535561100211405ustar00rootroot00000000000000smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/lib.rs000066400000000000000000000250171462535561100222610ustar00rootroot00000000000000/* * Copyright (c) 2020-2024, Stalwart Labs Ltd. * * Licensed under the Apache License, Version 2.0 or the MIT license * , at your * option. This file may not be copied, modified, or distributed * except according to those terms. */ //! # 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; pub mod response; mod tokens; #[cfg(feature = "serde_support")] use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Request { Ehlo { host: T }, Lhlo { host: T }, Helo { host: T }, Mail { from: MailFrom }, Rcpt { to: RcptTo }, Bdat { chunk_size: usize, is_last: bool }, Auth { mechanism: u64, initial_response: T }, Noop { value: T }, Vrfy { value: T }, Expn { value: T }, Help { value: T }, Etrn { name: T }, Atrn { domains: Vec }, Burl { uri: T, is_last: bool }, StartTls, Data, Rset, Quit, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct MailFrom { pub address: T, pub flags: u64, pub size: usize, pub trans_id: Option, pub by: i64, pub env_id: Option, pub solicit: Option, pub mtrk: Option>, pub auth: Option, pub hold_for: u64, pub hold_until: u64, pub mt_priority: i64, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RcptTo { pub address: T, pub orcpt: Option, pub rrvs: i64, pub flags: u64, } pub const MAIL_BODY_7BIT: u64 = 1 << 0; pub const MAIL_BODY_8BITMIME: u64 = 1 << 1; pub const MAIL_BODY_BINARYMIME: u64 = 1 << 2; pub const MAIL_RET_FULL: u64 = 1 << 3; pub const MAIL_RET_HDRS: u64 = 1 << 4; pub const MAIL_SMTPUTF8: u64 = 1 << 5; pub const MAIL_REQUIRETLS: u64 = 1 << 6; pub const MAIL_CONPERM: u64 = 1 << 7; pub const MAIL_BY_NOTIFY: u64 = 1 << 8; pub const MAIL_BY_RETURN: u64 = 1 << 9; pub const MAIL_BY_TRACE: u64 = 1 << 10; pub const RCPT_NOTIFY_SUCCESS: u64 = 1 << 0; pub const RCPT_NOTIFY_FAILURE: u64 = 1 << 1; pub const RCPT_NOTIFY_DELAY: u64 = 1 << 2; pub const RCPT_NOTIFY_NEVER: u64 = 1 << 3; pub const RCPT_CONNEG: u64 = 1 << 4; pub const RCPT_RRVS_REJECT: u64 = 1 << 5; pub const RCPT_RRVS_CONTINUE: u64 = 1 << 6; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Mtrk { pub certifier: T, pub timeout: u64, } pub const AUTH_SCRAM_SHA_256_PLUS: u64 = 1u64 << 0; pub const AUTH_SCRAM_SHA_256: u64 = 1u64 << 1; pub const AUTH_SCRAM_SHA_1_PLUS: u64 = 1u64 << 2; pub const AUTH_SCRAM_SHA_1: u64 = 1u64 << 3; pub const AUTH_OAUTHBEARER: u64 = 1u64 << 4; pub const AUTH_XOAUTH: u64 = 1u64 << 5; pub const AUTH_XOAUTH2: u64 = 1u64 << 6; pub const AUTH_9798_M_DSA_SHA1: u64 = 1u64 << 7; pub const AUTH_9798_M_ECDSA_SHA1: u64 = 1u64 << 8; pub const AUTH_9798_M_RSA_SHA1_ENC: u64 = 1u64 << 9; pub const AUTH_9798_U_DSA_SHA1: u64 = 1u64 << 10; pub const AUTH_9798_U_ECDSA_SHA1: u64 = 1u64 << 11; pub const AUTH_9798_U_RSA_SHA1_ENC: u64 = 1u64 << 12; pub const AUTH_EAP_AES128: u64 = 1u64 << 13; pub const AUTH_EAP_AES128_PLUS: u64 = 1u64 << 14; pub const AUTH_ECDH_X25519_CHALLENGE: u64 = 1u64 << 15; pub const AUTH_ECDSA_NIST256P_CHALLENGE: u64 = 1u64 << 16; pub const AUTH_EXTERNAL: u64 = 1u64 << 17; pub const AUTH_GS2_KRB5: u64 = 1u64 << 18; pub const AUTH_GS2_KRB5_PLUS: u64 = 1u64 << 19; pub const AUTH_GSS_SPNEGO: u64 = 1u64 << 20; pub const AUTH_GSSAPI: u64 = 1u64 << 21; pub const AUTH_KERBEROS_V4: u64 = 1u64 << 22; pub const AUTH_KERBEROS_V5: u64 = 1u64 << 23; pub const AUTH_NMAS_SAMBA_AUTH: u64 = 1u64 << 24; pub const AUTH_NMAS_AUTHEN: u64 = 1u64 << 25; pub const AUTH_NMAS_LOGIN: u64 = 1u64 << 26; pub const AUTH_NTLM: u64 = 1u64 << 27; pub const AUTH_OAUTH10A: u64 = 1u64 << 28; pub const AUTH_OPENID20: u64 = 1u64 << 29; pub const AUTH_OTP: u64 = 1u64 << 30; pub const AUTH_SAML20: u64 = 1u64 << 31; pub const AUTH_SECURID: u64 = 1u64 << 32; pub const AUTH_SKEY: u64 = 1u64 << 33; pub const AUTH_SPNEGO: u64 = 1u64 << 34; pub const AUTH_SPNEGO_PLUS: u64 = 1u64 << 35; pub const AUTH_SXOVER_PLUS: u64 = 1u64 << 36; pub const AUTH_CRAM_MD5: u64 = 1u64 << 37; pub const AUTH_DIGEST_MD5: u64 = 1u64 << 38; pub const AUTH_LOGIN: u64 = 1u64 << 39; pub const AUTH_PLAIN: u64 = 1u64 << 40; pub const AUTH_ANONYMOUS: u64 = 1u64 << 41; pub const EXT_8BIT_MIME: u32 = 1 << 0; pub const EXT_ATRN: u32 = 1 << 1; pub const EXT_AUTH: u32 = 1 << 2; pub const EXT_BINARY_MIME: u32 = 1 << 3; pub const EXT_BURL: u32 = 1 << 4; pub const EXT_CHECKPOINT: u32 = 1 << 5; pub const EXT_CHUNKING: u32 = 1 << 6; pub const EXT_CONNEG: u32 = 1 << 7; pub const EXT_CONPERM: u32 = 1 << 8; pub const EXT_DELIVER_BY: u32 = 1 << 9; pub const EXT_DSN: u32 = 1 << 10; pub const EXT_ENHANCED_STATUS_CODES: u32 = 1 << 11; pub const EXT_ETRN: u32 = 1 << 12; pub const EXT_FUTURE_RELEASE: u32 = 1 << 13; pub const EXT_HELP: u32 = 1 << 14; pub const EXT_MT_PRIORITY: u32 = 1 << 15; pub const EXT_MTRK: u32 = 1 << 16; pub const EXT_NO_SOLICITING: u32 = 1 << 17; pub const EXT_ONEX: u32 = 1 << 18; pub const EXT_PIPELINING: u32 = 1 << 19; pub const EXT_REQUIRE_TLS: u32 = 1 << 20; pub const EXT_RRVS: u32 = 1 << 21; pub const EXT_SIZE: u32 = 1 << 22; pub const EXT_SMTP_UTF8: u32 = 1 << 23; pub const EXT_START_TLS: u32 = 1 << 24; pub const EXT_VERB: u32 = 1 << 25; pub const EXT_EXPN: u32 = 1 << 26; pub const EXT_VRFY: u32 = 1 << 27; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum MtPriority { #[default] Mixer, Stanag4406, Nsep, } #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct EhloResponse { pub hostname: T, pub capabilities: u32, pub auth_mechanisms: u64, pub deliver_by: u64, pub future_release_interval: u64, pub future_release_datetime: u64, pub mt_priority: MtPriority, pub no_soliciting: Option, pub size: usize, } #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub struct Response { pub code: u16, pub esc: [u8; 3], pub message: T, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Severity { PositiveCompletion = 2, PositiveIntermediate = 3, TransientNegativeCompletion = 4, PermanentNegativeCompletion = 5, Invalid = 0, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Category { Syntax = 0, Information = 1, Connections = 2, Unspecified3 = 3, Unspecified4 = 4, MailSystem = 5, Invalid = 6, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Error { NeedsMoreData { bytes_left: usize }, UnknownCommand, InvalidSenderAddress, InvalidRecipientAddress, SyntaxError { syntax: &'static str }, InvalidParameter { param: &'static str }, UnsupportedParameter { param: String }, ResponseTooLong, InvalidResponse { code: u16 }, } pub(crate) const LF: u8 = b'\n'; pub(crate) const SP: u8 = b' '; pub trait IntoString: Sized { fn into_string(self) -> String; } impl IntoString for Vec { fn into_string(self) -> String { String::from_utf8(self) .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) } } impl Default for MailFrom { fn default() -> Self { Self { address: Default::default(), flags: Default::default(), size: Default::default(), trans_id: Default::default(), by: Default::default(), env_id: Default::default(), solicit: Default::default(), mtrk: Default::default(), auth: Default::default(), hold_for: Default::default(), hold_until: Default::default(), mt_priority: Default::default(), } } } impl Default for RcptTo { fn default() -> Self { Self { address: Default::default(), orcpt: Default::default(), rrvs: Default::default(), flags: Default::default(), } } } impl AsRef> for EhloResponse { fn as_ref(&self) -> &EhloResponse { self } } impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::NeedsMoreData { bytes_left } => { write!(f, "Needs more data: {} bytes left", bytes_left) } Error::UnknownCommand => write!(f, "Unknown command"), Error::InvalidSenderAddress => write!(f, "Invalid sender address"), Error::InvalidRecipientAddress => write!(f, "Invalid recipient address"), Error::SyntaxError { syntax } => write!(f, "Syntax error: {}", syntax), Error::InvalidParameter { param } => write!(f, "Invalid parameter: {}", param), Error::UnsupportedParameter { param } => { write!(f, "Unsupported parameter: {}", param) } Error::ResponseTooLong => write!(f, "Response too long"), Error::InvalidResponse { code } => write!(f, "Invalid response: {}", code), } } } impl std::error::Error for Error {} smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/request/000077500000000000000000000000001462535561100226305ustar00rootroot00000000000000smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/request/mod.rs000066400000000000000000000046721462535561100237660ustar00rootroot00000000000000/* * Copyright (c) 2020-2024, Stalwart Labs Ltd. * * Licensed under the Apache License, Version 2.0 or the MIT license * , at your * option. This file may not be copied, modified, or distributed * except according to those terms. */ use crate::tokens::{define_tokens_128, define_tokens_64}; pub mod parser; pub mod receiver; pub const AUTH: u64 = crate::tokens::token64("AUTH"); // special, because it is `pub` instead of `pub(crate)` define_tokens_64! { // SMTP commands EHLO, HELO, LHLO, MAIL, RCPT, DATA, BDAT, RSET, VRFY, EXPN, HELP, NOOP, QUIT, ETRN, ATRN, BURL, STARTTLS, // Arguments FROM, TO, LAST, // Parameters N, NT, C, R, RT, FULL, HDRS, RFC822, } define_tokens_128! { // Parameters BODY, SEVENBIT = "7BIT", EIGHBITMIME = "8BITMIME", BINARYMIME, SIZE, TRANSID, BY, NOTIFY, ORCPT, RET, ENVID, NEVER, SUCCESS, FAILURE, DELAY, SOLICIT, MTRK, AUTH_ = "AUTH", HOLDFOR, HOLDUNTIL, SMTPUTF8, CONPERM, CONNEG, MT_PRIORITY = "MT-PRIORITY", RRVS, REQUIRETLS, _9798_M_DSA_SHA1 = "9798-M-DSA-SHA1", _9798_M_ECDSA_SHA = "9798-M-ECDSA-SHA", _9798_M_RSA_SHA1_ = "9798-M-RSA-SHA1-", _9798_U_DSA_SHA1 = "9798-U-DSA-SHA1", _9798_U_ECDSA_SHA = "9798-U-ECDSA-SHA", _9798_U_RSA_SHA1_ = "9798-U-RSA-SHA1-", ANONYMOUS, CRAM_MD5 = "CRAM-MD5", DIGEST_MD5 = "DIGEST-MD5", EAP_AES128 = "EAP-AES128", EAP_AES128_PLUS = "EAP-AES128-PLUS", ECDH_X25519_CHAL = "ECDH-X25519-CHAL", ECDSA_NIST256P_C = "ECDSA-NIST256P-C", EXTERNAL, GS2_KRB5 = "GS2-KRB5", GS2_KRB5_PLUS = "GS2-KRB5-PLUS", GSS_SPNEGO = "GSS-SPNEGO", GSSAPI, KERBEROS_V4 = "KERBEROS-V4", KERBEROS_V5 = "KERBEROS-V5", LOGIN, NMAS_SAMBA_AUTH = "NMAS-SAMBA-AUTH", NMAS_AUTHEN = "NMAS-AUTHEN", NMAS_LOGIN = "NMAS-LOGIN", NTLM, OAUTH10A, OAUTHBEARER, OPENID20, OTP, PLAIN, SAML20, SCRAM_SHA_1 = "SCRAM-SHA-1", SCRAM_SHA_1_PLUS = "SCRAM-SHA-1-PLUS", SCRAM_SHA_256 = "SCRAM-SHA-256", SCRAM_SHA_256_PL = "SCRAM-SHA-256-PL", SECURID, SKEY, SPNEGO, SPNEGO_PLUS = "SPNEGO-PLUS", SXOVER_PLUS = "SXOVER-PLUS", XOAUTH, XOAUTH2, } smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/request/parser.rs000066400000000000000000002227741462535561100245100ustar00rootroot00000000000000/* * Copyright (c) 2020-2024, Stalwart Labs Ltd. * * Licensed under the Apache License, Version 2.0 or the MIT license * , at your * option. This file may not be copied, modified, or distributed * except according to those terms. */ use std::slice::Iter; use crate::*; 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); let command = parser.hashed_value()?; if !parser.stop_char.is_ascii_whitespace() { parser.seek_lf()?; return Err(Error::UnknownCommand); } match command { RCPT => { if !(parser.stop_char == LF || parser.hashed_value()? != TO || parser.stop_char != b':' && parser.next_char()? != b':') && parser.next_char()? == b'<' { if let Some(to) = parser.address()? { if parser.stop_char == b'>' { return Ok(Request::Rcpt { to: parser.rcpt_to_parameters(to)?, }); } } else { parser.seek_lf()?; return Err(Error::InvalidRecipientAddress); } } parser.seek_lf()?; Err(Error::SyntaxError { syntax: "RCPT TO: [parameters]", }) } MAIL => { if !(parser.stop_char == LF || parser.hashed_value()? != FROM || parser.stop_char != b':' && parser.next_char()? != b':') && parser.next_char()? == b'<' { if let Some(from) = parser.address()? { if parser.stop_char == b'>' { return Ok(Request::Mail { from: parser.mail_from_parameters(from)?, }); } } else { parser.seek_lf()?; return Err(Error::InvalidSenderAddress); } } parser.seek_lf()?; Err(Error::SyntaxError { syntax: "MAIL FROM: [parameters]", }) } DATA => { parser.seek_lf()?; Ok(Request::Data) } EHLO => { if parser.stop_char != LF { let host = parser.text()?; parser.seek_lf()?; if (1..=MAX_DOMAIN_LEN).contains(&host.len()) { return Ok(Request::Ehlo { host }); } } Err(Error::SyntaxError { syntax: "EHLO domain", }) } BDAT => { if parser.stop_char != LF { let chunk_size = parser.size()?; if chunk_size != usize::MAX && parser.stop_char.is_ascii_whitespace() { if parser.stop_char != LF { match parser.hashed_value()? { LAST => { parser.seek_lf()?; return Ok(Request::Bdat { chunk_size, is_last: true, }); } 0 => { parser.seek_lf()?; return Ok(Request::Bdat { chunk_size, is_last: false, }); } _ => (), } } else { return Ok(Request::Bdat { chunk_size, is_last: false, }); } } } parser.seek_lf()?; Err(Error::SyntaxError { syntax: "BDAT chunk-size [LAST]", }) } AUTH => { if parser.stop_char != LF { if let Some(mechanism) = parser.mechanism()? { let initial_response = if parser.stop_char != LF { parser.text()? } else { String::new() }; parser.seek_lf()?; return Ok(Request::Auth { mechanism, initial_response, }); } } Err(Error::SyntaxError { syntax: "AUTH mechanism [initial-response]", }) } EXPN => { if parser.stop_char != LF { let value = parser.string()?; parser.seek_lf()?; if (1..=MAX_ADDRESS_LEN).contains(&value.len()) { return Ok(Request::Expn { value }); } } Err(Error::SyntaxError { syntax: "EXPN string", }) } NOOP => { if parser.stop_char != LF { let value = parser.string()?; parser.seek_lf()?; Ok(Request::Noop { value }) } else { Ok(Request::Noop { value: String::new(), }) } } QUIT => { parser.seek_lf()?; Ok(Request::Quit) } LHLO => { if parser.stop_char != LF { let host = parser.text()?; parser.seek_lf()?; if (1..=MAX_DOMAIN_LEN).contains(&host.len()) { return Ok(Request::Lhlo { host }); } } Err(Error::SyntaxError { syntax: "LHLO domain", }) } RSET => { parser.seek_lf()?; Ok(Request::Rset) } VRFY => { if parser.stop_char != LF { let value = parser.string()?; parser.seek_lf()?; if (1..=MAX_ADDRESS_LEN).contains(&value.len()) { return Ok(Request::Vrfy { value }); } } Err(Error::SyntaxError { syntax: "VRFY string", }) } HELP => { if parser.stop_char != LF { let value = parser.string()?; parser.seek_lf()?; Ok(Request::Help { value }) } else { Ok(Request::Help { value: String::new(), }) } } STARTTLS => { parser.seek_lf()?; Ok(Request::StartTls) } ETRN => { if parser.stop_char != LF { let name = parser.string()?; parser.seek_lf()?; if !name.is_empty() { return Ok(Request::Etrn { name }); } } Err(Error::SyntaxError { syntax: "ETRN name", }) } ATRN => { if parser.stop_char != LF { let mut domains = Vec::new(); loop { let domain = parser.seek_char(b',')?; if !domain.is_empty() { domains.push(domain); } if parser.stop_char != b',' { parser.seek_lf()?; if !domains.is_empty() { return Ok(Request::Atrn { domains }); } else { break; } } } } Err(Error::SyntaxError { syntax: "ATRN domain[,domain]", }) } BURL => { if parser.stop_char != LF { let uri = parser.text()?; if !uri.is_empty() && parser.stop_char.is_ascii_whitespace() { if parser.stop_char != LF { match parser.hashed_value()? { LAST => { parser.seek_lf()?; return Ok(Request::Burl { uri, is_last: true }); } 0 => { parser.seek_lf()?; return Ok(Request::Burl { uri, is_last: false, }); } _ => (), } } else { return Ok(Request::Burl { uri, is_last: false, }); } } } parser.seek_lf()?; Err(Error::SyntaxError { syntax: "BURL absolute-uri [LAST]", }) } HELO => { if parser.stop_char != LF { let host = parser.text()?; parser.seek_lf()?; if (1..=MAX_DOMAIN_LEN).contains(&host.len()) { return Ok(Request::Helo { host }); } } Err(Error::SyntaxError { syntax: "HELO domain", }) } _ => { parser.seek_lf()?; Err(Error::UnknownCommand) } } } } pub struct Rfc5321Parser<'x, 'y> { bytes: &'x mut Iter<'y, u8>, pub stop_char: u8, pub bytes_left: usize, } impl<'x, 'y> Rfc5321Parser<'x, 'y> { pub fn new(bytes: &'x mut Iter<'y, u8>) -> Self { let (bytes_left, _) = bytes.size_hint(); Rfc5321Parser { bytes, bytes_left, stop_char: 0, } } #[allow(clippy::while_let_on_iterator)] pub fn hashed_value(&mut self) -> Result { let mut value: u64 = 0; let mut shift = 0; while let Some(&ch) = self.bytes.next() { match ch { b'A'..=b'Z' | b'0'..=b'9' | b'-' if shift < 64 => { value |= (ch as u64) << shift; shift += 8; } b'a'..=b'z' if shift < 64 => { value |= ((ch - b'a' + b'A') as u64) << shift; shift += 8; } b'\r' => (), b' ' => { if value != 0 { self.stop_char = ch; return Ok(value); } } _ => { self.stop_char = ch; return Ok(value); } } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } #[allow(clippy::while_let_on_iterator)] pub fn hashed_value_long(&mut self) -> Result { let mut value: u128 = 0; let mut shift = 0; while let Some(&ch) = self.bytes.next() { match ch { b'A'..=b'Z' | b'0'..=b'9' | b'-' if shift < 128 => { value |= (ch as u128) << shift; shift += 8; } b'a'..=b'z' if shift < 128 => { value |= ((ch - b'a' + b'A') as u128) << shift; shift += 8; } b' ' => { if value != 0 { self.stop_char = b' '; return Ok(value); } } b'\r' => (), _ => { self.stop_char = ch; return Ok(value); } } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } pub fn address(&mut self) -> Result, Error> { let mut value = Vec::with_capacity(32); let mut last_ch = 0; let mut in_quote = false; let mut at_count = 0; let mut lp_len = 0; for &ch in &mut self.bytes { match ch { b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'/' | b'=' | b'?' | b'^' | b'_' | b'`' | b'{' | b'|' | b'}' | b'~' | 0x7f..=u8::MAX => { value.push(ch); } b'.' if !in_quote => { if last_ch != b'.' && last_ch != b'@' && !value.is_empty() { value.push(ch); } else { self.stop_char = ch; return Ok(None); } } b'@' if !in_quote => { at_count += 1; lp_len = value.len(); value.push(ch); } b'>' if !in_quote => { self.stop_char = ch; let value = value.into_string(); let len = value.chars().count(); 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'@')) => { // Remove source route value.clear(); at_count = 0; lp_len = 0; } b',' if !in_quote && matches!(value.first(), Some(b'@')) => (), b' ' if !in_quote => { if !value.is_empty() { self.stop_char = b' '; let value = value.into_string(); let len = value.chars().count(); 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 <= MAX_ADDRESS_LEN && at_count == 1 && lp_len > 0 { value.into() } else { None }, ); } b'\"' if !in_quote || last_ch != b'\\' => { in_quote = !in_quote; } b'\\' if in_quote && last_ch != b'\\' => (), _ => { if in_quote { value.push(ch); } else { self.stop_char = ch; return Ok(None); } } } last_ch = ch; } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } pub fn string(&mut self) -> Result { let mut in_quote = false; let mut value = Vec::with_capacity(32); let mut last_ch = 0; for &ch in &mut self.bytes { match ch { b' ' if !in_quote => { if !value.is_empty() { self.stop_char = b' '; return Ok(value.into_string()); } } b'\n' => { self.stop_char = b'\n'; return Ok(value.into_string()); } b'\"' if !in_quote || last_ch != b'\\' => { in_quote = !in_quote; } b'\\' if in_quote && last_ch != b'\\' => (), b'\r' => (), _ => { value.push(ch); } } last_ch = ch; } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } #[allow(clippy::while_let_on_iterator)] pub fn text(&mut self) -> Result { let mut value = Vec::with_capacity(32); while let Some(&ch) = self.bytes.next() { match ch { b'\n' => { self.stop_char = b'\n'; return Ok(value.into_string()); } b' ' => { self.stop_char = b' '; return Ok(value.into_string()); } b'\r' => (), _ => { value.push(ch); } } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } #[allow(clippy::while_let_on_iterator)] pub fn xtext(&mut self) -> Result { let mut value = Vec::with_capacity(32); while let Some(&ch) = self.bytes.next() { match ch { b'\n' => { self.stop_char = b'\n'; return Ok(value.into_string()); } b'+' => { let mut hex1 = None; while let Some(&ch) = self.bytes.next() { if let Some(digit) = char::from(ch).to_digit(16) { if let Some(hex1) = hex1 { value.push(((hex1 as u8) << 4) | digit as u8); break; } else { hex1 = Some(digit); } } else if ch == LF { self.stop_char = b'\n'; return Ok(value.into_string()); } else { break; } } } b' ' => { self.stop_char = b' '; return Ok(value.into_string()); } b'\r' => (), _ => { value.push(ch); } } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } #[allow(clippy::while_let_on_iterator)] pub fn seek_char(&mut self, stop_char: u8) -> Result { let mut value = Vec::with_capacity(32); while let Some(&ch) = self.bytes.next() { match ch { b'\n' => { self.stop_char = b'\n'; return Ok(value.into_string()); } b' ' => { if !value.is_empty() { self.stop_char = b' '; return Ok(value.into_string()); } } b'\r' => (), _ => { if ch != stop_char { value.push(ch); } else { self.stop_char = ch; return Ok(value.into_string()); } } } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } #[inline(always)] pub fn seek_lf(&mut self) -> Result<(), Error> { if self.stop_char != LF { for &ch in &mut self.bytes { if ch == LF { return Ok(()); } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } else { Ok(()) } } #[inline(always)] pub fn next_char(&mut self) -> Result { for &ch in &mut self.bytes { match ch { b' ' | b'\r' => (), _ => { self.stop_char = ch; return Ok(ch); } } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } #[inline(always)] pub fn read_char(&mut self) -> Result { self.bytes.next().copied().ok_or(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } pub fn size(&mut self) -> Result { let mut value = usize::MAX; for &ch in &mut self.bytes { match ch { b'0'..=b'9' => { value = if value != usize::MAX { value .saturating_mul(10) .saturating_add((ch - b'0') as usize) } else { (ch - b'0') as usize }; } b'\r' => (), b' ' => { if value != usize::MAX { self.stop_char = b' '; return Ok(value); } } _ => { self.stop_char = ch; return Ok(value); } } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } pub fn integer(&mut self) -> Result { let mut value = i64::MAX; let mut multiplier = 1; for &ch in &mut self.bytes { match ch { b'0'..=b'9' => { value = if value != i64::MAX { value.saturating_mul(10).saturating_add((ch - b'0') as i64) } else { (ch - b'0') as i64 }; } b' ' => { if value != i64::MAX { self.stop_char = b' '; return Ok(value * multiplier); } } b'-' if value == i64::MAX => { multiplier = -1; } b'+' if value == i64::MAX => (), b'\r' => (), _ => { self.stop_char = ch; return Ok(if value != i64::MAX { value * multiplier } else { i64::MAX }); } } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } pub fn timestamp(&mut self) -> Result { let mut dt = [0u32; 8]; let mut zone_multiplier = 1; let mut pos = 0; for &ch in &mut self.bytes { match ch { b'0'..=b'9' if pos < 8 => { dt[pos] = dt[pos] .saturating_mul(10) .saturating_add((ch - b'0') as u32); } b'-' if pos <= 1 || pos == 5 => { pos += 1; } b'+' if pos == 5 => { zone_multiplier = -1; pos += 1; } b'T' if pos == 2 => { pos += 1; } b':' if pos == 3 || pos == 4 || pos == 6 => { pos += 1; } b'Z' if pos == 5 => { pos = 8; } _ => { self.stop_char = ch; return Ok(if pos >= 7 { // Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992 let month = dt[1]; let year_base = 4800; /* Before min year, multiple of 400. */ let m_adj = month.wrapping_sub(3); /* March-based month. */ let carry = i64::from(m_adj > month); let adjust = if carry > 0 { 12 } else { 0 }; let y_adj = dt[0] as i64 + year_base - carry; let month_days = ((m_adj.wrapping_add(adjust)) * 62719 + 769) / 2048; let leap_days = y_adj / 4 - y_adj / 100 + y_adj / 400; (y_adj * 365 + leap_days + month_days as i64 + (dt[2] as i64 - 1) - 2472632) * 86400 + dt[3] as i64 * 3600 + dt[4] as i64 * 60 + dt[5] as i64 + ((dt[6] as i64 * 3600 + dt[7] as i64 * 60) * zone_multiplier) } else { i64::MAX }); } } } Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }) } pub fn mail_from_parameters(&mut self, address: String) -> Result, Error> { let mut params = MailFrom { address, flags: 0, size: 0, trans_id: None, by: 0, env_id: None, solicit: None, mtrk: None, auth: None, hold_for: 0, hold_until: 0, mt_priority: 0, }; while self.stop_char != LF { let key = self.hashed_value_long()?; match key { SMTPUTF8 if self.stop_char.is_ascii_whitespace() => { params.flags |= MAIL_SMTPUTF8; } BODY if self.stop_char == b'=' => { params.flags |= match self.hashed_value_long()? { EIGHBITMIME if self.stop_char.is_ascii_whitespace() => MAIL_BODY_8BITMIME, BINARYMIME if self.stop_char.is_ascii_whitespace() => MAIL_BODY_BINARYMIME, SEVENBIT if self.stop_char.is_ascii_whitespace() => MAIL_BODY_7BIT, _ => { self.seek_lf()?; return Err(Error::InvalidParameter { param: "BODY" }); } } } SIZE if self.stop_char == b'=' => { let size = self.size()?; if size != usize::MAX && self.stop_char.is_ascii_whitespace() { params.size = size; } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "SIZE" }); } } BY if self.stop_char == b'=' => { let time = self.integer()?; if time != i64::MAX && self.stop_char == b';' { params.flags |= match self.hashed_value()? { N if self.stop_char.is_ascii_whitespace() => MAIL_BY_NOTIFY, NT if self.stop_char.is_ascii_whitespace() => { MAIL_BY_NOTIFY | MAIL_BY_TRACE } R if self.stop_char.is_ascii_whitespace() => MAIL_BY_RETURN, RT if self.stop_char.is_ascii_whitespace() => { MAIL_BY_RETURN | MAIL_BY_TRACE } _ => { self.seek_lf()?; return Err(Error::InvalidParameter { param: "BY" }); } }; params.by = time; } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "BY" }); } } HOLDUNTIL if self.stop_char == b'=' => { let hold = self.size()?; if hold != usize::MAX && self.stop_char.is_ascii_whitespace() { params.hold_until = hold as u64; } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "HOLDUNTIL" }); } } HOLDFOR if self.stop_char == b'=' => { let hold = self.size()?; if hold != usize::MAX && self.stop_char.is_ascii_whitespace() { params.hold_for = hold as u64; } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "HOLDFOR" }); } } RET if self.stop_char == b'=' => { params.flags |= match self.hashed_value()? { FULL if self.stop_char.is_ascii_whitespace() => MAIL_RET_FULL, HDRS if self.stop_char.is_ascii_whitespace() => MAIL_RET_HDRS, _ => { self.seek_lf()?; return Err(Error::InvalidParameter { param: "RET" }); } }; } ENVID if self.stop_char == b'=' => { let env_id = self.xtext()?; if self.stop_char.is_ascii_whitespace() && (1..=100).contains(&env_id.len()) { params.env_id = env_id.into(); } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "ENVID" }); } } REQUIRETLS if self.stop_char.is_ascii_whitespace() => { params.flags |= MAIL_REQUIRETLS; } SOLICIT if self.stop_char == b'=' => { let solicit = self.text()?; if !solicit.is_empty() && self.stop_char.is_ascii_whitespace() { params.solicit = solicit.into(); } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "SOLICIT" }); } } TRANSID if self.stop_char == b'=' => { if self.next_char()? == b'<' { let transid = self.seek_char(b'>')?; if self.stop_char == b'>' && !transid.is_empty() { params.trans_id = transid.into(); self.stop_char = SP; continue; } } self.seek_lf()?; return Err(Error::InvalidParameter { param: "TRANSID" }); } MTRK if self.stop_char == b'=' => { let certifier = self.seek_char(b':')?; let timeout = if self.stop_char == b':' { self.size()? } else { 0 }; if !certifier.is_empty() && self.stop_char.is_ascii_whitespace() && timeout != usize::MAX { params.mtrk = Mtrk { certifier, timeout: timeout as u64, } .into(); } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "MTRK" }); } } AUTH_ if self.stop_char == b'=' => { let mailbox = self.xtext()?; if (1..=MAX_ADDRESS_LEN).contains(&mailbox.len()) && self.stop_char.is_ascii_whitespace() { params.auth = mailbox.into(); } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "AUTH" }); } } MT_PRIORITY if self.stop_char == b'=' => { let priority = self.integer()?; if priority != i64::MAX && self.stop_char.is_ascii_whitespace() { params.mt_priority = priority; } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "MT-PRIORITY", }); } } CONPERM if self.stop_char.is_ascii_whitespace() => { params.flags |= MAIL_CONPERM; } 0 => (), unknown => { let mut param = Vec::with_capacity(16); for ch in unknown.to_le_bytes() { if ch != 0 { param.push(ch.to_ascii_uppercase()); } } if !self.stop_char.is_ascii_whitespace() { param.push(self.stop_char.to_ascii_uppercase()); for &ch in &mut self.bytes { if !ch.is_ascii_whitespace() { param.push(ch.to_ascii_uppercase()); } else { self.stop_char = ch; break; } } } self.seek_lf()?; return Err(Error::UnsupportedParameter { param: param.into_string(), }); } } } Ok(params) } pub fn rcpt_to_parameters(&mut self, address: String) -> Result, Error> { let mut params = RcptTo { address, orcpt: None, rrvs: 0, flags: 0, }; while self.stop_char != LF { let key = self.hashed_value_long()?; match key { NOTIFY if self.stop_char == b'=' => loop { match self.hashed_value_long()? { NEVER if (params.flags & (RCPT_NOTIFY_NEVER | RCPT_NOTIFY_SUCCESS | RCPT_NOTIFY_FAILURE | RCPT_NOTIFY_DELAY)) == 0 => { params.flags |= RCPT_NOTIFY_NEVER; } SUCCESS => { params.flags |= RCPT_NOTIFY_SUCCESS; } FAILURE => { params.flags |= RCPT_NOTIFY_FAILURE; } DELAY => { params.flags |= RCPT_NOTIFY_DELAY; } _ => { self.seek_lf()?; return Err(Error::InvalidParameter { param: "NOTIFY" }); } } if self.stop_char.is_ascii_whitespace() { break; } else if self.stop_char != b',' { self.seek_lf()?; return Err(Error::InvalidParameter { param: "NOTIFY" }); } }, ORCPT if self.stop_char == b'=' => { let v = self.hashed_value()?; if v != RFC822 || self.stop_char != b';' { self.seek_lf()?; return Err(Error::InvalidParameter { param: "ORCPT" }); } let addr = self.xtext()?; if self.stop_char.is_ascii_whitespace() && (1..=MAX_ADDRESS_LEN).contains(&addr.len()) { params.orcpt = addr.into(); } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "ORCPT" }); } } RRVS if self.stop_char == b'=' => { let time = self.timestamp()?; if time != i64::MAX && self.stop_char.is_ascii_whitespace() || self.stop_char == b';' { let is_reject = self.stop_char != b';' || match self.hashed_value()? { C if self.stop_char.is_ascii_whitespace() => false, R if self.stop_char.is_ascii_whitespace() => true, _ => { self.seek_lf()?; return Err(Error::InvalidParameter { param: "RRVS" }); } }; params.rrvs = time; params.flags |= if is_reject { RCPT_RRVS_REJECT } else { RCPT_RRVS_CONTINUE }; } else { self.seek_lf()?; return Err(Error::InvalidParameter { param: "RRVS" }); } } CONNEG if self.stop_char.is_ascii_whitespace() => { params.flags |= RCPT_CONNEG; } 0 => (), unknown => { let mut param = Vec::with_capacity(16); for ch in unknown.to_le_bytes() { if ch != 0 { param.push(ch.to_ascii_uppercase()); } } if !self.stop_char.is_ascii_whitespace() { param.push(self.stop_char.to_ascii_uppercase()); for &ch in &mut self.bytes { if !ch.is_ascii_whitespace() { param.push(ch.to_ascii_uppercase()); } else { self.stop_char = ch; break; } } } self.seek_lf()?; return Err(Error::UnsupportedParameter { param: param.into_string(), }); } } } Ok(params) } pub fn mechanism(&mut self) -> Result, Error> { let mut trailing_chars = [0u8; 8]; let mut pos = 0; let mechanism = self.hashed_value_long()?; if !self.stop_char.is_ascii_whitespace() { trailing_chars[0] = self.stop_char; pos += 1; for &ch in &mut self.bytes { if !ch.is_ascii_whitespace() { if let Some(tch) = trailing_chars.get_mut(pos) { *tch = ch.to_ascii_uppercase(); } pos += 1; } else { self.stop_char = ch; break; } } if !self.stop_char.is_ascii_whitespace() { return Err(Error::NeedsMoreData { bytes_left: self.bytes_left, }); } else if pos > 8 { return Ok(0.into()); } } Ok(match (mechanism, &trailing_chars[..pos]) { (_9798_M_DSA_SHA1, b"") => AUTH_9798_M_DSA_SHA1.into(), (_9798_M_ECDSA_SHA, b"1") => AUTH_9798_M_ECDSA_SHA1.into(), (_9798_M_RSA_SHA1_, b"ENC") => AUTH_9798_M_RSA_SHA1_ENC.into(), (_9798_U_DSA_SHA1, b"") => AUTH_9798_U_DSA_SHA1.into(), (_9798_U_ECDSA_SHA, b"1") => AUTH_9798_U_ECDSA_SHA1.into(), (_9798_U_RSA_SHA1_, b"ENC") => AUTH_9798_U_RSA_SHA1_ENC.into(), (ANONYMOUS, b"") => AUTH_ANONYMOUS.into(), (CRAM_MD5, b"") => AUTH_CRAM_MD5.into(), (DIGEST_MD5, b"") => AUTH_DIGEST_MD5.into(), (EAP_AES128, b"") => AUTH_EAP_AES128.into(), (EAP_AES128_PLUS, b"") => AUTH_EAP_AES128_PLUS.into(), (ECDH_X25519_CHAL, b"LENGE") => AUTH_ECDH_X25519_CHALLENGE.into(), (ECDSA_NIST256P_C, b"HALLENGE") => AUTH_ECDSA_NIST256P_CHALLENGE.into(), (EXTERNAL, b"") => AUTH_EXTERNAL.into(), (GS2_KRB5, b"") => AUTH_GS2_KRB5.into(), (GS2_KRB5_PLUS, b"") => AUTH_GS2_KRB5_PLUS.into(), (GSS_SPNEGO, b"") => AUTH_GSS_SPNEGO.into(), (GSSAPI, b"") => AUTH_GSSAPI.into(), (KERBEROS_V4, b"") => AUTH_KERBEROS_V4.into(), (KERBEROS_V5, b"") => AUTH_KERBEROS_V5.into(), (LOGIN, b"") => AUTH_LOGIN.into(), (NMAS_SAMBA_AUTH, b"") => AUTH_NMAS_SAMBA_AUTH.into(), (NMAS_AUTHEN, b"") => AUTH_NMAS_AUTHEN.into(), (NMAS_LOGIN, b"") => AUTH_NMAS_LOGIN.into(), (NTLM, b"") => AUTH_NTLM.into(), (OAUTH10A, b"") => AUTH_OAUTH10A.into(), (OAUTHBEARER, b"") => AUTH_OAUTHBEARER.into(), (OPENID20, b"") => AUTH_OPENID20.into(), (OTP, b"") => AUTH_OTP.into(), (PLAIN, b"") => AUTH_PLAIN.into(), (SAML20, b"") => AUTH_SAML20.into(), (SCRAM_SHA_1, b"") => AUTH_SCRAM_SHA_1.into(), (SCRAM_SHA_1_PLUS, b"") => AUTH_SCRAM_SHA_1_PLUS.into(), (SCRAM_SHA_256, b"") => AUTH_SCRAM_SHA_256.into(), (SCRAM_SHA_256_PL, b"US") => AUTH_SCRAM_SHA_256_PLUS.into(), (SECURID, b"") => AUTH_SECURID.into(), (SKEY, b"") => AUTH_SKEY.into(), (SPNEGO, b"") => AUTH_SPNEGO.into(), (SPNEGO_PLUS, b"") => AUTH_SPNEGO_PLUS.into(), (SXOVER_PLUS, b"") => AUTH_SXOVER_PLUS.into(), (XOAUTH, b"") => AUTH_XOAUTH.into(), (XOAUTH2, b"") => AUTH_XOAUTH2.into(), (0, b"") => None, _ => 0.into(), }) } } #[cfg(test)] mod tests { use crate::*; #[test] fn parse_request() { for item in [ // HELO et al. ( "EHLO bar.com", Ok(Request::Ehlo { host: "bar.com".to_string(), }), ), ( "EHLO", Err(Error::SyntaxError { syntax: "EHLO domain", }), ), ( "HELO bar.com", Ok(Request::Helo { host: "bar.com".to_string(), }), ), ( "HELO", Err(Error::SyntaxError { syntax: "HELO domain", }), ), ( "LHLO bar.com", Ok(Request::Lhlo { host: "bar.com".to_string(), }), ), ( "LHLO", Err(Error::SyntaxError { syntax: "LHLO domain", }), ), // VRFY ( "VRFY Hello", Ok(Request::Vrfy { value: "Hello".to_string(), }), ), ( "VRFY \"Hello\\\" Wo\\\\rld\"", Ok(Request::Vrfy { value: "Hello\" Wo\\rld".to_string(), }), ), ( "VRFY \"\"", Err(Error::SyntaxError { syntax: "VRFY string", }), ), ( "VRFY", Err(Error::SyntaxError { syntax: "VRFY string", }), ), // EXPN ( "EXPN Hello", Ok(Request::Expn { value: "Hello".to_string(), }), ), ( "EXPN \"Hello\\\" Wo\\\\rld\"", Ok(Request::Expn { value: "Hello\" Wo\\rld".to_string(), }), ), ( "EXPN \"\"", Err(Error::SyntaxError { syntax: "EXPN string", }), ), ( "EXPN", Err(Error::SyntaxError { syntax: "EXPN string", }), ), // NOOP ( "NOOP", Ok(Request::Noop { value: "".to_string(), }), ), ( "NOOP Hello", Ok(Request::Noop { value: "Hello".to_string(), }), ), // HELP ( "HELP", Ok(Request::Help { value: "".to_string(), }), ), ( "HELP Hello", Ok(Request::Help { value: "Hello".to_string(), }), ), // No param commands ("DATA", Ok(Request::Data)), ("QUIT", Ok(Request::Quit)), ("RSET", Ok(Request::Rset)), ("STARTTLS", Ok(Request::StartTls)), // BDAT ( "BDAT 0", Ok(Request::Bdat { chunk_size: 0, is_last: false, }), ), ( "BDAT 123456", Ok(Request::Bdat { chunk_size: 123456, is_last: false, }), ), ( "BDAT 123456 LAST", Ok(Request::Bdat { chunk_size: 123456, is_last: true, }), ), ( "BDAT", Err(Error::SyntaxError { syntax: "BDAT chunk-size [LAST]", }), ), ( "BDAT 123LAST", Err(Error::SyntaxError { syntax: "BDAT chunk-size [LAST]", }), ), ( "BDAT 123x LAST", Err(Error::SyntaxError { syntax: "BDAT chunk-size [LAST]", }), ), ( "BDAT LAST", Err(Error::SyntaxError { syntax: "BDAT chunk-size [LAST]", }), ), // AUTH ( "AUTH GSSAPI", Ok(Request::Auth { mechanism: AUTH_GSSAPI, initial_response: "".to_string(), }), ), ( "AUTH ECDSA-NIST256P-CHALLENGE =", Ok(Request::Auth { mechanism: AUTH_ECDSA_NIST256P_CHALLENGE, initial_response: "=".to_string(), }), ), ( "AUTH SCRAM-SHA-256-PLUS base64_goes_here", Ok(Request::Auth { mechanism: AUTH_SCRAM_SHA_256_PLUS, initial_response: "base64_goes_here".to_string(), }), ), ( "AUTH ECDSA-NIST256P-CHALLENGE100 abcde", Ok(Request::Auth { mechanism: 0, initial_response: "abcde".to_string(), }), ), ( "AUTH", Err(Error::SyntaxError { syntax: "AUTH mechanism [initial-response]", }), ), // ETRN ( "ETRN Hello", Ok(Request::Etrn { name: "Hello".to_string(), }), ), ( "ETRN \"Hello\\\" Wo\\\\rld\"", Ok(Request::Etrn { name: "Hello\" Wo\\rld".to_string(), }), ), ( "ETRN \"\"", Err(Error::SyntaxError { syntax: "ETRN name", }), ), ( "ETRN", Err(Error::SyntaxError { syntax: "ETRN name", }), ), // ATRN ( "ATRN example.org", Ok(Request::Atrn { domains: vec!["example.org".to_string()], }), ), ( "ATRN example.org,example.com,example.net", Ok(Request::Atrn { domains: vec![ "example.org".to_string(), "example.com".to_string(), "example.net".to_string(), ], }), ), ( "ATRN example.org, example.com, example.net", Ok(Request::Atrn { domains: vec![ "example.org".to_string(), "example.com".to_string(), "example.net".to_string(), ], }), ), ( "ATRN", Err(Error::SyntaxError { syntax: "ATRN domain[,domain]", }), ), // BURL ( concat!( "BURL imap://harry@gryffindor.example.com/outbox", ";uidvalidity=1078863300/;uid=25;urlauth=submit+harry", ":internal:91354a473744909de610943775f92038 LAST" ), Ok(Request::Burl { uri: concat!( "imap://harry@gryffindor.example.com/outbox", ";uidvalidity=1078863300/;uid=25;urlauth=submit+harry", ":internal:91354a473744909de610943775f92038" ) .to_string(), is_last: true, }), ), ( "BURL imap:://test.example.org", Ok(Request::Burl { uri: "imap:://test.example.org".to_string(), is_last: false, }), ), ( "BURL", Err(Error::SyntaxError { syntax: "BURL absolute-uri [LAST]", }), ), // MAIL FROM ( "MAIL FROM:", Ok(Request::Mail { from: "JQP@bar.com".into(), }), ), ( "MAIL FROM:<@a,@b:user@d>", Ok(Request::Mail { from: "user@d".into(), }), ), ( "MAIL FROM:<\"@a,@b:\"@d>", Ok(Request::Mail { from: "@a,@b:@d".into(), }), ), ( "MAIL FROM: <\" hi there! \"@d>", Ok(Request::Mail { from: " hi there! @d".into(), }), ), ("MAIL FROM : <>", Ok(Request::Mail { from: "".into() })), ("MAIL FROM : < >", Ok(Request::Mail { from: "".into() })), ( "MAIL FROM:", Ok(Request::Mail { from: "hi.there@valid.org".into(), }), ), ("MAIL FROM:<@invalid>", Err(Error::InvalidSenderAddress)), ( "MAIL FROM:", Err(Error::InvalidSenderAddress), ), ( "MAIL FROM:", Err(Error::InvalidSenderAddress), ), ( "MAIL FROM:", Err(Error::InvalidSenderAddress), ), ( "MAIL FROM:", Err(Error::InvalidSenderAddress), ), ( "MAIL FROM:<.hi.there@invalid.org>", Err(Error::InvalidSenderAddress), ), ("MAIL FROM:<@>", Err(Error::InvalidSenderAddress)), ("MAIL FROM:<.@.>", Err(Error::InvalidSenderAddress)), ( "RCPT TO:<孫子@áéíóú.org>", Ok(Request::Rcpt { to: "孫子@áéíóú.org".into(), }), ), // RCPT TO ( "RCPT TO:", Ok(Request::Rcpt { to: "Jones@XYZ.COM".into(), }), ), ("RCPT TO:<>", Ok(Request::Rcpt { to: "".into() })), // Invalid commands ("", Err(Error::UnknownCommand)), ("X-SPECIAL", Err(Error::UnknownCommand)), ("DATA_", Err(Error::UnknownCommand)), // Invalid parameters ( "MAIL FROM:<> HELLO=WORLD", Err(Error::UnsupportedParameter { param: "HELLO=WORLD".to_string(), }), ), ( "MAIL FROM:<> VERY_LONG_AND_INVALID=PARAM", Err(Error::UnsupportedParameter { param: "VERY_LONG_AND_INVALID=PARAM".to_string(), }), ), ( "MAIL FROM:<> SMTPUTF8=YES", Err(Error::UnsupportedParameter { param: "SMTPUTF8=YES".to_string(), }), ), ( "MAIL FROM:<> SMTPUTF8=YES", Err(Error::UnsupportedParameter { param: "SMTPUTF8=YES".to_string(), }), ), // Parameters ( "MAIL FROM:<> SMTPUTF8", Ok(Request::Mail { from: MailFrom { address: "".to_string(), flags: MAIL_SMTPUTF8, ..Default::default() }, }), ), ( "MAIL FROM:<> SMTPUTF8 REQUIRETLS CONPERM", Ok(Request::Mail { from: MailFrom { address: "".to_string(), flags: MAIL_SMTPUTF8 | MAIL_REQUIRETLS | MAIL_CONPERM, ..Default::default() }, }), ), ( "RCPT TO:<> CONNEG", Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), flags: RCPT_CONNEG, ..Default::default() }, }), ), ( "MAIL FROM:<> BODY=BINARYMIME BODY=7BIT BODY=8BITMIME", Ok(Request::Mail { from: MailFrom { address: "".to_string(), flags: MAIL_BODY_7BIT | MAIL_BODY_8BITMIME | MAIL_BODY_BINARYMIME, ..Default::default() }, }), ), ( "MAIL FROM:<> BODY=OTHER", Err(Error::InvalidParameter { param: "BODY" }), ), ( "MAIL FROM:<> SIZE=500000", Ok(Request::Mail { from: MailFrom { address: "".to_string(), size: 500000, ..Default::default() }, }), ), ( "MAIL FROM:<> SIZE=ABC", Err(Error::InvalidParameter { param: "SIZE" }), ), ( "MAIL FROM:<> SIZE=-100", Err(Error::InvalidParameter { param: "SIZE" }), ), ( "MAIL FROM:<> SIZE=", Err(Error::InvalidParameter { param: "SIZE" }), ), ( "MAIL FROM:<> BY=120;R", Ok(Request::Mail { from: MailFrom { address: "".to_string(), by: 120, flags: MAIL_BY_RETURN, ..Default::default() }, }), ), ( "MAIL FROM:<> BY=0;N", Ok(Request::Mail { from: MailFrom { address: "".to_string(), by: 0, flags: MAIL_BY_NOTIFY, ..Default::default() }, }), ), ( "MAIL FROM:<> BY=-10;RT", Ok(Request::Mail { from: MailFrom { address: "".to_string(), by: -10, flags: MAIL_BY_RETURN | MAIL_BY_TRACE, ..Default::default() }, }), ), ( "MAIL FROM:<> BY=+22;NT", Ok(Request::Mail { from: MailFrom { address: "".to_string(), by: 22, flags: MAIL_BY_NOTIFY | MAIL_BY_TRACE, ..Default::default() }, }), ), ( "MAIL FROM:<> BY=120", Err(Error::InvalidParameter { param: "BY" }), ), ( "MAIL FROM:<> BY=120;T", Err(Error::InvalidParameter { param: "BY" }), ), ( "MAIL FROM:<> BY=120;", Err(Error::InvalidParameter { param: "BY" }), ), ( "MAIL FROM:<> BY=120;0", Err(Error::InvalidParameter { param: "BY" }), ), ( "MAIL FROM:<> BY=120;;", Err(Error::InvalidParameter { param: "BY" }), ), ( "MAIL FROM:<> BY=;", Err(Error::InvalidParameter { param: "BY" }), ), ( "MAIL FROM:<> BY=;R", Err(Error::InvalidParameter { param: "BY" }), ), ( "MAIL FROM:<> BY=", Err(Error::InvalidParameter { param: "BY" }), ), ( "MAIL FROM:<> HOLDUNTIL=12345 HOLDFOR=67890", Ok(Request::Mail { from: MailFrom { address: "".to_string(), hold_for: 67890, hold_until: 12345, ..Default::default() }, }), ), ( "MAIL FROM:<> HOLDUNTIL=0ABC", Err(Error::InvalidParameter { param: "HOLDUNTIL" }), ), ( "MAIL FROM:<> HOLDUNTIL=", Err(Error::InvalidParameter { param: "HOLDUNTIL" }), ), ( "MAIL FROM:<> HOLDFOR=XYZ", Err(Error::InvalidParameter { param: "HOLDFOR" }), ), ( "MAIL FROM:<> HOLDFOR=", Err(Error::InvalidParameter { param: "HOLDFOR" }), ), ( concat!("RCPT TO:<> NOTIFY=FAILURE"), Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), flags: RCPT_NOTIFY_FAILURE, ..Default::default() }, }), ), ( concat!("RCPT TO:<> NOTIFY=FAILURE,DELAY"), Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), flags: RCPT_NOTIFY_FAILURE | RCPT_NOTIFY_DELAY, ..Default::default() }, }), ), ( concat!("RCPT TO:<> NOTIFY=SUCCESS,FAILURE,DELAY"), Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), flags: RCPT_NOTIFY_FAILURE | RCPT_NOTIFY_DELAY | RCPT_NOTIFY_SUCCESS, ..Default::default() }, }), ), ( concat!("RCPT TO:<> NOTIFY=NEVER"), Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), flags: RCPT_NOTIFY_NEVER, ..Default::default() }, }), ), ( "RCPT TO:<> NOTIFY=", Err(Error::InvalidParameter { param: "NOTIFY" }), ), ( "RCPT TO:<> NOTIFY=FAILURE,NEVER", Err(Error::InvalidParameter { param: "NOTIFY" }), ), ( "RCPT TO:<> NOTIFY=CHIMICHANGA", Err(Error::InvalidParameter { param: "NOTIFY" }), ), ( concat!("RCPT TO:<> ORCPT=rfc822;Bob@Example.COM"), Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), orcpt: "Bob@Example.COM".to_string().into(), ..Default::default() }, }), ), ( concat!("RCPT TO:<> ", "ORCPT=rfc822;George+20@Tax-+20ME+20.GOV"), Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), orcpt: "George @Tax- ME .GOV".to_string().into(), ..Default::default() }, }), ), ( "RCPT TO:<> ORCPT=", Err(Error::InvalidParameter { param: "ORCPT" }), ), ( "RCPT TO:<> ORCPT=;hello@domain.org", Err(Error::InvalidParameter { param: "ORCPT" }), ), ( "RCPT TO:<> ORCPT=rfc822;", Err(Error::InvalidParameter { param: "ORCPT" }), ), ( "RCPT TO:<> ORCPT=;", Err(Error::InvalidParameter { param: "ORCPT" }), ), ( "MAIL FROM:<> RET=HDRS RET=FULL", Ok(Request::Mail { from: MailFrom { address: "".to_string(), flags: MAIL_RET_FULL | MAIL_RET_HDRS, ..Default::default() }, }), ), ( "MAIL FROM:<> RET=", Err(Error::InvalidParameter { param: "RET" }), ), ( "MAIL FROM:<> RET=ENCHILADA", Err(Error::InvalidParameter { param: "RET" }), ), ( "MAIL FROM:<> ENVID=QQ314159", Ok(Request::Mail { from: MailFrom { address: "".to_string(), env_id: "QQ314159".to_string().into(), ..Default::default() }, }), ), ( "MAIL FROM:<> ENVID=hi+20there", Ok(Request::Mail { from: MailFrom { address: "".to_string(), env_id: "hi there".to_string().into(), ..Default::default() }, }), ), ( "MAIL FROM:<> ENVID=", Err(Error::InvalidParameter { param: "ENVID" }), ), ( concat!("MAIL FROM:<> SOLICIT=org.example:ADV:ADLT",), Ok(Request::Mail { from: MailFrom { address: "".to_string(), solicit: "org.example:ADV:ADLT".to_string().into(), ..Default::default() }, }), ), ( concat!( "MAIL FROM:<> ", " SOLICIT=net.example:ADV,org.example:ADV:ADLT" ), Ok(Request::Mail { from: MailFrom { address: "".to_string(), solicit: "net.example:ADV,org.example:ADV:ADLT".to_string().into(), ..Default::default() }, }), ), ( "MAIL FROM:<> SOLICIT=", Err(Error::InvalidParameter { param: "SOLICIT" }), ), ( "MAIL FROM:<> TRANSID=<12345@claremont.edu>", Ok(Request::Mail { from: MailFrom { address: "".to_string(), trans_id: "12345@claremont.edu".to_string().into(), ..Default::default() }, }), ), ( "MAIL FROM:<> TRANSID=", Err(Error::InvalidParameter { param: "TRANSID" }), ), ( "MAIL FROM:<> MTRK=my-ceritifier", Ok(Request::Mail { from: MailFrom { address: "".to_string(), mtrk: Mtrk { certifier: "my-ceritifier".to_string(), timeout: 0, } .into(), ..Default::default() }, }), ), ( "MAIL FROM:<> MTRK=other-certifier:1234", Ok(Request::Mail { from: MailFrom { address: "".to_string(), mtrk: Mtrk { certifier: "other-certifier".to_string(), timeout: 1234, } .into(), ..Default::default() }, }), ), ( "MAIL FROM:<> MTRK=", Err(Error::InvalidParameter { param: "MTRK" }), ), ( "MAIL FROM:<> MTRK=:", Err(Error::InvalidParameter { param: "MTRK" }), ), ( "MAIL FROM:<> MTRK=:998", Err(Error::InvalidParameter { param: "MTRK" }), ), ( "MAIL FROM:<> MTRK=abc:", Err(Error::InvalidParameter { param: "MTRK" }), ), ( "MAIL FROM:<> MTRK=abc:abc", Err(Error::InvalidParameter { param: "MTRK" }), ), ( "MAIL FROM:<> AUTH=<>", Ok(Request::Mail { from: MailFrom { address: "".to_string(), auth: "<>".to_string().into(), ..Default::default() }, }), ), ( "MAIL FROM:<> AUTH=e+3Dmc2@example.com", Ok(Request::Mail { from: MailFrom { address: "".to_string(), auth: "e=mc2@example.com".to_string().into(), ..Default::default() }, }), ), ( "MAIL FROM:<> AUTH=", Err(Error::InvalidParameter { param: "AUTH" }), ), ( "MAIL FROM:<> MT-PRIORITY=3", Ok(Request::Mail { from: MailFrom { address: "".to_string(), mt_priority: 3, ..Default::default() }, }), ), ( "MAIL FROM:<> MT-PRIORITY=-6", Ok(Request::Mail { from: MailFrom { address: "".to_string(), mt_priority: -6, ..Default::default() }, }), ), ( "MAIL FROM:<> MT-PRIORITY=", Err(Error::InvalidParameter { param: "MT-PRIORITY", }), ), ( "MAIL FROM:<> MT-PRIORITY=ab", Err(Error::InvalidParameter { param: "MT-PRIORITY", }), ), ( "MAIL FROM:<> MT-PRIORITY=-", Err(Error::InvalidParameter { param: "MT-PRIORITY", }), ), ( concat!("RCPT TO:<> RRVS=2014-04-03T23:01:00Z"), Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), rrvs: 1396566060, flags: RCPT_RRVS_REJECT, ..Default::default() }, }), ), ( concat!("RCPT TO:<> RRVS=1997-11-24T14:22:01-08:00;C"), Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), rrvs: 880410121, flags: RCPT_RRVS_CONTINUE, ..Default::default() }, }), ), ( concat!("RCPT TO:<> RRVS=2003-07-01T10:52:37+02:00;R"), Ok(Request::Rcpt { to: RcptTo { address: "".to_string(), rrvs: 1057049557, flags: RCPT_RRVS_REJECT, ..Default::default() }, }), ), ( "RCPT TO:<> RRVS=", Err(Error::InvalidParameter { param: "RRVS" }), ), ( "RCPT TO:<> RRVS=2022-01-02", Err(Error::InvalidParameter { param: "RRVS" }), ), ( "RCPT TO:<> RRVS=2022-01-02T01:01:01", Err(Error::InvalidParameter { param: "RRVS" }), ), ( "RCPT TO:<> RRVS=2022-01-02T01:01:01ZZ", Err(Error::InvalidParameter { param: "RRVS" }), ), ( "RCPT TO:<> RRVS=ABC", Err(Error::InvalidParameter { param: "RRVS" }), ), ] { let (request, parsed_request): (&str, Result, Error>) = item; for extra in ["\n", "\r\n", " \n", " \r\n"] { let request = format!("{request}{extra}"); assert_eq!( parsed_request, Request::parse(&mut request.as_bytes().iter()), "failed for {request:?}" ); } } } impl From<&str> for MailFrom { fn from(value: &str) -> Self { Self { address: value.to_string(), ..Default::default() } } } impl From<&str> for RcptTo { fn from(value: &str) -> Self { Self { address: value.to_string(), ..Default::default() } } } } smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/request/receiver.rs000066400000000000000000000243741462535561100250140ustar00rootroot00000000000000/* * Copyright (c) 2020-2024, Stalwart Labs Ltd. * * Licensed under the Apache License, Version 2.0 or the MIT license * , at your * option. This file may not be copied, modified, or distributed * except according to those terms. */ use std::slice::Iter; use crate::{Error, Request}; pub const MAX_LINE_LENGTH: usize = 2048; #[derive(Default)] pub struct RequestReceiver { pub buf: Vec, } pub struct DataReceiver { crlf_dot: bool, last_ch: u8, prev_last_ch: u8, } pub struct BdatReceiver { pub is_last: bool, bytes_left: usize, } pub struct DummyDataReceiver { is_bdat: bool, bdat_bytes_left: usize, crlf_dot: bool, last_ch: u8, prev_last_ch: u8, } #[derive(Default)] pub struct DummyLineReceiver {} #[derive(Default)] pub struct LineReceiver { pub buf: Vec, pub state: T, } impl RequestReceiver { pub fn ingest( &mut self, bytes: &mut Iter<'_, u8>, buf: &[u8], ) -> Result, Error> { if self.buf.is_empty() { match Request::parse(bytes) { Err(Error::NeedsMoreData { bytes_left }) => { if bytes_left > 0 { if bytes_left < MAX_LINE_LENGTH { self.buf = buf[buf.len().saturating_sub(bytes_left)..].to_vec(); } else { return Err(Error::ResponseTooLong); } } } result => return result, } } else { for &ch in bytes { self.buf.push(ch); if ch == b'\n' { let result = Request::parse(&mut self.buf.iter()); self.buf.clear(); return result; } else if self.buf.len() == MAX_LINE_LENGTH { self.buf.clear(); return Err(Error::ResponseTooLong); } } } Err(Error::NeedsMoreData { bytes_left: 0 }) } } impl DataReceiver { #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { crlf_dot: false, last_ch: 0, prev_last_ch: 0, } } pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>, buf: &mut Vec) -> bool { for &ch in bytes { match ch { b'.' if self.last_ch == b'\n' && self.prev_last_ch == b'\r' => { self.crlf_dot = true; } b'\n' if self.crlf_dot && self.last_ch == b'\r' => { buf.truncate(buf.len() - 3); return true; } b'\r' => { buf.push(ch); } _ => { buf.push(ch); self.crlf_dot = false; } } self.prev_last_ch = self.last_ch; self.last_ch = ch; } false } } impl BdatReceiver { pub fn new(chunk_size: usize, is_last: bool) -> Self { Self { bytes_left: chunk_size, is_last, } } pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>, buf: &mut Vec) -> bool { while self.bytes_left > 0 { if let Some(&ch) = bytes.next() { buf.push(ch); self.bytes_left -= 1; } else { return false; } } true } } impl DummyDataReceiver { pub fn new_bdat(chunk_size: usize) -> Self { Self { bdat_bytes_left: chunk_size, is_bdat: true, crlf_dot: false, last_ch: 0, prev_last_ch: 0, } } pub fn new_data(data: &DataReceiver) -> Self { Self { is_bdat: false, bdat_bytes_left: 0, crlf_dot: data.crlf_dot, last_ch: data.last_ch, prev_last_ch: data.prev_last_ch, } } pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>) -> bool { if !self.is_bdat { for &ch in bytes { match ch { b'.' if self.last_ch == b'\n' && self.prev_last_ch == b'\r' => { self.crlf_dot = true; } b'\n' if self.crlf_dot && self.last_ch == b'\r' => { return true; } b'\r' => {} _ => { self.crlf_dot = false; } } self.prev_last_ch = self.last_ch; self.last_ch = ch; } false } else { while self.bdat_bytes_left > 0 { if bytes.next().is_some() { self.bdat_bytes_left -= 1; } else { return false; } } true } } } impl LineReceiver { pub fn new(state: T) -> Self { Self { buf: Vec::with_capacity(32), state, } } pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>) -> bool { for &ch in bytes { match ch { b'\n' => return true, b'\r' => (), _ => { if self.buf.len() < MAX_LINE_LENGTH { self.buf.push(ch); } } } } false } } impl DummyLineReceiver { pub fn ingest(&mut self, bytes: &mut Iter<'_, u8>) -> bool { for &ch in bytes { if ch == b'\n' { return true; } } false } } #[cfg(test)] mod tests { use crate::{request::receiver::RequestReceiver, Error, Request}; use super::DataReceiver; #[test] fn data_receiver() { 'outer: for (data, message) in [ ( vec!["hi\r\n", "..\r\n", ".a\r\n", "\r\n.\r\n"], "hi\r\n.\r\na\r\n", ), ( vec!["\r\na\rb\nc\r\n.d\r\n..\r\n", "\r\n.\r\n"], "\r\na\rb\nc\r\nd\r\n.\r\n", ), // Test SMTP smuggling attempts ( vec![ "\n.\r\n", "MAIL FROM:\r\n", "RCPT TO:\r\n", "RCPT TO:\r\n", "RCPT TO:\r\n", "RCPT TO:\r\n", "RCPT TO:\r\n", "RCPT TO:\r\n", "RCPT TO:\r\n", "RCPT TO: { requests.push(request); continue; } Err(Error::NeedsMoreData { .. }) => { break; } err => panic!("Unexpected error {err:?}"), } } } assert_eq!(expected_requests, requests); } } } smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/response/000077500000000000000000000000001462535561100227765ustar00rootroot00000000000000smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/response/generate.rs000066400000000000000000001423171462535561100251460ustar00rootroot00000000000000/* * Copyright (c) 2020-2024, Stalwart Labs Ltd. * * Licensed under the Apache License, Version 2.0 or the MIT license * , at your * option. This file may not be copied, modified, or distributed * except according to those terms. */ use std::{ fmt::Display, io::{self, Write}, }; use crate::*; impl EhloResponse { pub fn new(hostname: T) -> Self { Self { hostname, capabilities: 0, auth_mechanisms: 0, deliver_by: 0, future_release_interval: 0, future_release_datetime: 0, mt_priority: MtPriority::Mixer, no_soliciting: None, size: 0, } } pub fn write(&self, mut writer: impl Write) -> io::Result<()> { write!(writer, "250-{} you had me at EHLO\r\n", self.hostname)?; let mut capabilities = self.capabilities; while capabilities != 0 { let capability = 1 << (31 - capabilities.leading_zeros()); capabilities ^= capability; writer.write_all(b"250")?; writer.write_all(if capabilities != 0 { b"-" } else { b" " })?; match capability { EXT_8BIT_MIME => write!(writer, "8BITMIME\r\n"), EXT_ATRN => write!(writer, "ATRN\r\n"), EXT_AUTH => { writer.write_all(b"AUTH")?; let mut mechanisms = self.auth_mechanisms; while mechanisms != 0 { let item = 1 << (63 - mechanisms.leading_zeros()); mechanisms ^= item; write!(writer, " {}", item.to_mechanism())?; } writer.write_all(b"\r\n") } EXT_BINARY_MIME => write!(writer, "BINARYMIME\r\n"), EXT_BURL => write!(writer, "BURL\r\n"), EXT_CHECKPOINT => write!(writer, "CHECKPOINT\r\n"), EXT_CHUNKING => write!(writer, "CHUNKING\r\n"), EXT_CONNEG => write!(writer, "CONNEG\r\n"), EXT_CONPERM => write!(writer, "CONPERM\r\n"), EXT_DELIVER_BY => { if self.deliver_by > 0 { write!(writer, "DELIVERBY {}\r\n", self.deliver_by) } else { write!(writer, "DELIVERBY\r\n") } } EXT_DSN => write!(writer, "DSN\r\n"), EXT_ENHANCED_STATUS_CODES => write!(writer, "ENHANCEDSTATUSCODES\r\n"), EXT_ETRN => write!(writer, "ETRN\r\n"), EXT_EXPN => write!(writer, "EXPN\r\n"), EXT_VRFY => write!(writer, "VRFY\r\n"), EXT_FUTURE_RELEASE => write!( writer, "FUTURERELEASE {} {}\r\n", self.future_release_interval, self.future_release_datetime ), EXT_HELP => write!(writer, "HELP\r\n"), EXT_MT_PRIORITY => write!( writer, "MT-PRIORITY {}\r\n", match self.mt_priority { MtPriority::Mixer => "MIXER", MtPriority::Stanag4406 => "STANAG4406", MtPriority::Nsep => "NSEP", } ), EXT_MTRK => write!(writer, "MTRK\r\n"), EXT_NO_SOLICITING => { if let Some(keywords) = &self.no_soliciting { write!(writer, "NO-SOLICITING {keywords}\r\n") } else { write!(writer, "NO-SOLICITING\r\n") } } EXT_ONEX => write!(writer, "ONEX\r\n"), EXT_PIPELINING => write!(writer, "PIPELINING\r\n"), EXT_REQUIRE_TLS => write!(writer, "REQUIRETLS\r\n"), EXT_RRVS => write!(writer, "RRVS\r\n"), EXT_SIZE => { if self.size > 0 { write!(writer, "SIZE {}\r\n", self.size) } else { write!(writer, "SIZE\r\n") } } EXT_SMTP_UTF8 => write!(writer, "SMTPUTF8\r\n"), EXT_START_TLS => write!(writer, "STARTTLS\r\n"), EXT_VERB => write!(writer, "VERB\r\n"), _ => write!(writer, ""), }?; } Ok(()) } } impl Response { pub fn write(&self, mut writer: impl Write) -> io::Result<()> { write!( writer, "{} {}.{}.{} {}\r\n", self.code, self.esc[0], self.esc[1], self.esc[2], self.message ) } } pub trait BitToString { fn to_mechanism(&self) -> &'static str; } impl BitToString for u64 { fn to_mechanism(&self) -> &'static str { match *self { AUTH_SCRAM_SHA_256_PLUS => "SCRAM-SHA-256-PLUS", AUTH_SCRAM_SHA_256 => "SCRAM-SHA-256", AUTH_SCRAM_SHA_1_PLUS => "SCRAM-SHA-1-PLUS", AUTH_SCRAM_SHA_1 => "SCRAM-SHA-1", AUTH_OAUTHBEARER => "OAUTHBEARER", AUTH_XOAUTH => "XOAUTH", AUTH_XOAUTH2 => "XOAUTH2", AUTH_9798_M_DSA_SHA1 => "9798-M-DSA-SHA1", AUTH_9798_M_ECDSA_SHA1 => "9798-M-ECDSA-SHA1", AUTH_9798_M_RSA_SHA1_ENC => "9798-M-RSA-SHA1-ENC", AUTH_9798_U_DSA_SHA1 => "9798-U-DSA-SHA1", AUTH_9798_U_ECDSA_SHA1 => "9798-U-ECDSA-SHA1", AUTH_9798_U_RSA_SHA1_ENC => "9798-U-RSA-SHA1-ENC", AUTH_EAP_AES128 => "EAP-AES128", AUTH_EAP_AES128_PLUS => "EAP-AES128-PLUS", AUTH_ECDH_X25519_CHALLENGE => "ECDH-X25519-CHALLENGE", AUTH_ECDSA_NIST256P_CHALLENGE => "ECDSA-NIST256P-CHALLENGE", AUTH_EXTERNAL => "EXTERNAL", AUTH_GS2_KRB5 => "GS2-KRB5", AUTH_GS2_KRB5_PLUS => "GS2-KRB5-PLUS", AUTH_GSS_SPNEGO => "GSS-SPNEGO", AUTH_GSSAPI => "GSSAPI", AUTH_KERBEROS_V4 => "KERBEROS_V4", AUTH_KERBEROS_V5 => "KERBEROS_V5", AUTH_NMAS_SAMBA_AUTH => "NMAS-SAMBA-AUTH", AUTH_NMAS_AUTHEN => "NMAS_AUTHEN", AUTH_NMAS_LOGIN => "NMAS_LOGIN", AUTH_NTLM => "NTLM", AUTH_OAUTH10A => "OAUTH10A", AUTH_OPENID20 => "OPENID20", AUTH_OTP => "OTP", AUTH_SAML20 => "SAML20", AUTH_SECURID => "SECURID", AUTH_SKEY => "SKEY", AUTH_SPNEGO => "SPNEGO", AUTH_SPNEGO_PLUS => "SPNEGO-PLUS", AUTH_SXOVER_PLUS => "SXOVER-PLUS", AUTH_CRAM_MD5 => "CRAM-MD5", AUTH_DIGEST_MD5 => "DIGEST-MD5", AUTH_LOGIN => "LOGIN", AUTH_PLAIN => "PLAIN", AUTH_ANONYMOUS => "ANONYMOUS", _ => "", } } } impl Response { pub fn new(code: u16, e0: u8, e1: u8, e2: u8, message: T) -> Self { Self { code, esc: [e0, e1, e2], message, } } /// Returns the reply's numeric status. pub fn code(&self) -> u16 { self.code } /// Returns the message included in the reply. pub fn message(&self) -> &T { &self.message } /// Returns the status severity (first digit of the status code). pub fn severity(&self) -> Severity { match self.code { 200..=299 => Severity::PositiveCompletion, 300..=399 => Severity::PositiveIntermediate, 400..=499 => Severity::TransientNegativeCompletion, 500..=599 => Severity::PermanentNegativeCompletion, _ => Severity::Invalid, } } /// Returns the status category (second digit of the status code). pub fn category(&self) -> Category { match (self.code / 10) % 10 { 0 => Category::Syntax, 1 => Category::Information, 2 => Category::Connections, 3 => Category::Unspecified3, 4 => Category::Unspecified4, 5 => Category::MailSystem, _ => Category::Invalid, } } /// Returns the status details (third digit of the status code). pub fn details(&self) -> u16 { self.code % 10 } /// Returns `true` if the reply is a positive completion. pub fn is_positive_completion(&self) -> bool { self.severity() == Severity::PositiveCompletion } pub fn explain_class_code(&self) -> Option<(&'static str, &'static str)> { match self.esc[0] { 2 => ( "Success", concat!( "Success specifies that the DSN is reporting a posi", "tive delivery action. Detail sub-codes may provide", " notification of transformations required for deli", "very." ), ) .into(), 4 => ( "Persistent Transient Failure", concat!( "A persistent transient failure is one in which the", " message as sent is valid, but persistence of some", " temporary condition has caused abandonment or del", "ay of attempts to send the message. If this code a", "ccompanies a delivery failure report, sending in t", "he future may be successful." ), ) .into(), 5 => ( "Permanent Failure", concat!( "A permanent failure is one which is not likely to ", "be resolved by resending the message in the curren", "t form. Some change to the message or the destinat", "ion must be made for successful delivery." ), ) .into(), _ => None, } } pub fn explain_subject_code(&self) -> Option<(&'static str, &'static str)> { match self.esc[1] { 0 => ( "Other or Undefined Status", concat!("There is no additional subject information availab", "le."), ) .into(), 1 => ( "Addressing Status", concat!( "The address status reports on the originator or de", "stination address. It may include address syntax o", "r validity. These errors can generally be correcte", "d by the sender and retried." ), ) .into(), 2 => ( "Mailbox Status", concat!( "Mailbox status indicates that something having to ", "do with the mailbox has caused this DSN. Mailbox i", "ssues are assumed to be under the general control ", "of the recipient." ), ) .into(), 3 => ( "Mail System Status", concat!( "Mail system status indicates that something having", " to do with the destination system has caused this", " DSN. System issues are assumed to be under the ge", "neral control of the destination system administra", "tor." ), ) .into(), 4 => ( "Network and Routing Status", concat!( "The networking or routing codes report status abou", "t the delivery system itself. These system compone", "nts include any necessary infrastructure such as d", "irectory and routing services. Network issues are ", "assumed to be under the control of the destination", " or intermediate system administrator." ), ) .into(), 5 => ( "Mail Delivery Protocol Status", concat!( "The mail delivery protocol status codes report fai", "lures involving the message delivery protocol. The", "se failures include the full range of problems res", "ulting from implementation errors or an unreliable", " connection." ), ) .into(), 6 => ( "Message Content or Media Status", concat!( "The message content or media status codes report f", "ailures involving the content of the message. Thes", "e codes report failures due to translation, transc", "oding, or otherwise unsupported message media. Mes", "sage content or media issues are under the control", " of both the sender and the receiver, both of whic", "h must support a common set of supported content-t", "ypes." ), ) .into(), 7 => ( "Security or Policy Status", concat!( "The security or policy status codes report failure", "s involving policies such as per-recipient or per-", "host filtering and cryptographic operations. Secur", "ity and policy status issues are assumed to be und", "er the control of either or both the sender and re", "cipient. Both the sender and recipient must permit", " the exchange of messages and arrange the exchange", " of necessary keys and certificates for cryptograp", "hic operations." ), ) .into(), _ => None, } } pub fn explain_status_code(&self) -> Option<(&'static str, &'static str)> { match (self.esc[1], self.esc[2]) { (0, 0) => ( "Other undefined Status", concat!( "Other undefined status is the only undefined error", " code." ), ) .into(), (1, 0) => ( "Other address status", concat!( "Something about the address specified in the messa", "ge caused this DSN." ), ) .into(), (1, 1) => ( "Bad destination mailbox address", concat!( "The mailbox specified in the address does not exis", "t. For Internet mail names, this means the address", " portion to the left of the \"@\" sign is invalid.", " This code is only useful for permanent failures." ), ) .into(), (1, 2) => ( "Bad destination system address", concat!( "The destination system specified in the address do", "es not exist or is incapable of accepting mail. Fo", "r Internet mail names, this means the address port", "ion to the right of the \"@\" is invalid for mail.", " This code is only useful for permanent failures." ), ) .into(), (1, 3) => ( "Bad destination mailbox address syntax", concat!( "The destination address was syntactically invalid.", " This can apply to any field in the address. This ", "code is only useful for permanent failures." ), ) .into(), (1, 4) => ( "Destination mailbox address ambiguous", concat!( "The mailbox address as specified matches one or mo", "re recipients on the destination system. This may ", "result if a heuristic address mapping algorithm is", " used to map the specified address to a local mail", "box name." ), ) .into(), (1, 5) => ( "Destination address valid", concat!( "This mailbox address as specified was valid. This ", "status code should be used for positive delivery r", "eports." ), ) .into(), (1, 6) => ( "Destination mailbox has moved, No forwarding address", concat!( "The mailbox address provided was at one time valid", ", but mail is no longer being accepted for that ad", "dress. This code is only useful for permanent fail", "ures." ), ) .into(), (1, 7) => ( "Bad sender's mailbox address syntax", concat!( "The sender's address was syntactically invalid. Th", "is can apply to any field in the address." ), ) .into(), (1, 8) => ( "Bad sender's system address", concat!( "The sender's system specified in the address does ", "not exist or is incapable of accepting return mail", ". For domain names, this means the address portion", " to the right of the \"@\" is invalid for mail." ), ) .into(), (1, 9) => ( "Message relayed to non-compliant mailer", concat!( "The mailbox address specified was valid, but the m", "essage has been relayed to a system that does not ", "speak this protocol; no further information can be", " provided." ), ) .into(), (1, 10) => ( "Recipient address has null MX", concat!( "This status code is returned when the associated a", "ddress is marked as invalid using a null MX." ), ) .into(), (2, 0) => ( "Other or undefined mailbox status", concat!( "The mailbox exists, but something about the destin", "ation mailbox has caused the sending of this DSN." ), ) .into(), (2, 1) => ( "Mailbox disabled, not accepting messages", concat!( "The mailbox exists, but is not accepting messages.", " This may be a permanent error if the mailbox will", " never be re-enabled or a transient error if the m", "ailbox is only temporarily disabled." ), ) .into(), (2, 2) => ( "Mailbox full", concat!( "The mailbox is full because the user has exceeded ", "a per-mailbox administrative quota or physical cap", "acity. The general semantics implies that the reci", "pient can delete messages to make more space avail", "able. This code should be used as a persistent tra", "nsient failure." ), ) .into(), (2, 3) => ( "Message length exceeds administrative limit", concat!( "A per-mailbox administrative message length limit ", "has been exceeded. This status code should be used", " when the per-mailbox message length limit is less", " than the general system limit. This code should b", "e used as a permanent failure." ), ) .into(), (2, 4) => ( "Mailing list expansion problem", concat!( "The mailbox is a mailing list address and the mail", "ing list was unable to be expanded. This code may ", "represent a permanent failure or a persistent tran", "sient failure." ), ) .into(), (3, 0) => ( "Other or undefined mail system status", concat!( "The destination system exists and normally accepts", " mail, but something about the system has caused t", "he generation of this DSN." ), ) .into(), (3, 1) => ( "Mail system full", concat!( "Mail system storage has been exceeded. The general", " semantics imply that the individual recipient may", " not be able to delete material to make room for a", "dditional messages." ), ) .into(), (3, 2) => ( "System not accepting network messages", concat!( "The host on which the mailbox is resident is not a", "ccepting messages. Examples of such conditions inc", "lude an imminent shutdown, excessive load, or syst", "em maintenance." ), ) .into(), (3, 3) => ( "System not capable of selected features", concat!( "Selected features specified for the message are no", "t supported by the destination system. This can oc", "cur in gateways when features from one domain cann", "ot be mapped onto the supported feature in another", "." ), ) .into(), (3, 4) => ( "Message too big for system", concat!( "The message is larger than per-message size limit.", " This limit may either be for physical or administ", "rative reasons." ), ) .into(), (3, 5) => ( "System incorrectly configured", concat!( "The system is not configured in a manner that will", " permit it to accept this message." ), ) .into(), (3, 6) => ( "Requested priority was changed", concat!( "The message was accepted for relay/delivery, but t", "he requested priority (possibly the implied defaul", "t) was not honoured. The human readable text after", " the status code contains the new priority, follow", "ed by SP (space) and explanatory human readable te", "xt." ), ) .into(), (4, 0) => ( "Other or undefined network or routing status", concat!( "Something went wrong with the networking, but it i", "s not clear what the problem is, or the problem ca", "nnot be well expressed with any of the other provi", "ded detail codes." ), ) .into(), (4, 1) => ( "No answer from host", concat!( "The outbound connection attempt was not answered, ", "because either the remote system was busy, or was ", "unable to take a call." ), ) .into(), (4, 2) => ( "Bad connection", concat!( "The outbound connection was established, but was u", "nable to complete the message transaction, either ", "because of time-out, or inadequate connection qual", "ity." ), ) .into(), (4, 3) => ( "Directory server failure", concat!( "The network system was unable to forward the messa", "ge, because a directory server was unavailable. Th", "e inability to connect to an Internet DNS server i", "s one example of the directory server failure erro", "r." ), ) .into(), (4, 4) => ( "Unable to route", concat!( "The mail system was unable to determine the next h", "op for the message because the necessary routing i", "nformation was unavailable from the directory serv", "er. A DNS lookup returning only an SOA (Start of A", "dministration) record for a domain name is one exa", "mple of the unable to route error." ), ) .into(), (4, 5) => ( "Mail system congestion", concat!( "The mail system was unable to deliver the message ", "because the mail system was congested." ), ) .into(), (4, 6) => ( "Routing loop detected", concat!( "A routing loop caused the message to be forwarded ", "too many times, either because of incorrect routin", "g tables or a user- forwarding loop." ), ) .into(), (4, 7) => ( "Delivery time expired", concat!( "The message was considered too old by the rejectin", "g system, either because it remained on that host ", "too long or because the time-to-live value specifi", "ed by the sender of the message was exceeded. If p", "ossible, the code for the actual problem found whe", "n delivery was attempted should be returned rather", " than this code." ), ) .into(), (5, 0) => ( "Other or undefined protocol status", concat!( "Something was wrong with the protocol necessary to", " deliver the message to the next hop and the probl", "em cannot be well expressed with any of the other ", "provided detail codes." ), ) .into(), (5, 1) => ( "Invalid command", concat!( "A mail transaction protocol command was issued whi", "ch was either out of sequence or unsupported." ), ) .into(), (5, 2) => ( "Syntax error", concat!( "A mail transaction protocol command was issued whi", "ch could not be interpreted, either because the sy", "ntax was wrong or the command is unrecognized." ), ) .into(), (5, 3) => ( "Too many recipients", concat!( "More recipients were specified for the message tha", "n could have been delivered by the protocol. This ", "error should normally result in the segmentation o", "f the message into two, the remainder of the recip", "ients to be delivered on a subsequent delivery att", "empt. It is included in this list in the event tha", "t such segmentation is not possible." ), ) .into(), (5, 4) => ( "Invalid command arguments", concat!( "A valid mail transaction protocol command was issu", "ed with invalid arguments, either because the argu", "ments were out of range or represented unrecognize", "d features." ), ) .into(), (5, 5) => ( "Wrong protocol version", concat!( "A protocol version mis-match existed which could n", "ot be automatically resolved by the communicating ", "parties." ), ) .into(), (5, 6) => ( "Authentication Exchange line is too long", concat!( "This enhanced status code SHOULD be returned when ", "the server fails the AUTH command due to the clien", "t sending a [BASE64] response which is longer than", " the maximum buffer size available for the current", "ly selected SASL mechanism." ), ) .into(), (6, 0) => ( "Other or undefined media error", concat!( "Something about the content of a message caused it", " to be considered undeliverable and the problem ca", "nnot be well expressed with any of the other provi", "ded detail codes." ), ) .into(), (6, 1) => ( "Media not supported", concat!( "The media of the message is not supported by eithe", "r the delivery protocol or the next system in the ", "forwarding path." ), ) .into(), (6, 2) => ( "Conversion required and prohibited", concat!( "The content of the message must be converted befor", "e it can be delivered and such conversion is not p", "ermitted. Such prohibitions may be the expression ", "of the sender in the message itself or the policy ", "of the sending host." ), ) .into(), (6, 3) => ( "Conversion required but not supported", concat!( "The message content must be converted in order to ", "be forwarded but such conversion is not possible o", "r is not practical by a host in the forwarding pat", "h. This condition may result when an ESMTP gateway", " supports 8bit transport but is not able to downgr", "ade the message to 7 bit as required for the next ", "hop." ), ) .into(), (6, 4) => ( "Conversion with loss performed", concat!( "This is a warning sent to the sender when message ", "delivery was successfully but when the delivery re", "quired a conversion in which some data was lost. T", "his may also be a permanent error if the sender ha", "s indicated that conversion with loss is prohibite", "d for the message." ), ) .into(), (6, 5) => ( "Conversion Failed", concat!( "A conversion was required but was unsuccessful. Th", "is may be useful as a permanent or persistent temp", "orary notification." ), ) .into(), (6, 6) => ( "Message content not available", concat!( "The message content could not be fetched from a re", "mote system. This may be useful as a permanent or ", "persistent temporary notification." ), ) .into(), (6, 7) => ( "Non-ASCII addresses not permitted for that sender/recipient", concat!( "This indicates the reception of a MAIL or RCPT com", "mand that non-ASCII addresses are not permitted" ), ) .into(), (6, 8 | 10) => ( "UTF-8 string reply is required, but not permitted by the SMTP client", concat!( "This indicates that a reply containing a UTF-8 str", "ing is required to show the mailbox name, but that", " form of response is not permitted by the SMTP cli", "ent." ), ) .into(), (6, 9) => ( concat!( "UTF-8 header message cannot be transferred to ", "one or more recipients, so the message must be rejected" ), concat!( "This indicates that transaction failed after the f", "inal \".\" of the DATA command." ), ) .into(), (7, 0) => ( "Other or undefined security status", concat!( "Something related to security caused the message t", "o be returned, and the problem cannot be well expr", "essed with any of the other provided detail codes.", " This status code may also be used when the condit", "ion cannot be further described because of securit", "y policies in force." ), ) .into(), (7, 1) => ( "Delivery not authorized, message refused", concat!( "The sender is not authorized to send to the destin", "ation. This can be the result of per-host or per-r", "ecipient filtering. This memo does not discuss the", " merits of any such filtering, but provides a mech", "anism to report such." ), ) .into(), (7, 2) => ( "Mailing list expansion prohibited", concat!( "The sender is not authorized to send a message to ", "the intended mailing list." ), ) .into(), (7, 3) => ( "Security conversion required but not possible", concat!( "A conversion from one secure messaging protocol to", " another was required for delivery and such conver", "sion was not possible." ), ) .into(), (7, 4) => ( "Security features not supported", concat!( "A message contained security features such as secu", "re authentication that could not be supported on t", "he delivery protocol." ), ) .into(), (7, 5) => ( "Cryptographic failure", concat!( "A transport system otherwise authorized to validat", "e or decrypt a message in transport was unable to ", "do so because necessary information such as key wa", "s not available or such information was invalid." ), ) .into(), (7, 6) => ( "Cryptographic algorithm not supported", concat!( "A transport system otherwise authorized to validat", "e or decrypt a message was unable to do so because", " the necessary algorithm was not supported." ), ) .into(), (7, 7) => ( "Message integrity failure", concat!( "A transport system otherwise authorized to validat", "e a message was unable to do so because the messag", "e was corrupted or altered. This may be useful as ", "a permanent, transient persistent, or successful d", "elivery code." ), ) .into(), (7, 8) => ( "Authentication credentials invalid", concat!( "This response to the AUTH command indicates that t", "he authentication failed due to invalid or insuffi", "cient authentication credentials. In this case, th", "e client SHOULD ask the user to supply new credent", "ials (such as by presenting a password dialog box)", "." ), ) .into(), (7, 9) => ( "Authentication mechanism is too weak", concat!( "This response to the AUTH command indicates that t", "he selected authentication mechanism is weaker tha", "n server policy permits for that user. The client ", "SHOULD retry with a new authentication mechanism." ), ) .into(), (7, 10) => ( "Encryption Needed", concat!( "This indicates that external strong privacy layer ", "is needed in order to use the requested authentica", "tion mechanism. This is primarily intended for use", " with clear text authentication mechanisms. A clie", "nt which receives this may activate a security lay", "er such as TLS prior to authenticating, or attempt", " to use a stronger mechanism." ), ) .into(), (7, 11) => ( "Encryption required for requested authentication mechanism", concat!( "This response to the AUTH command indicates that t", "he selected authentication mechanism may only be u", "sed when the underlying SMTP connection is encrypt", "ed. Note that this response code is documented her", "e for historical purposes only. Modern implementat", "ions SHOULD NOT advertise mechanisms that are not ", "permitted due to lack of encryption, unless an enc", "ryption layer of sufficient strength is currently ", "being employed." ), ) .into(), (7, 12) => ( "A password transition is needed", concat!( "This response to the AUTH command indicates that t", "he user needs to transition to the selected authen", "tication mechanism. This is typically done by auth", "enticating once using the [PLAIN] authentication m", "echanism. The selected mechanism SHOULD then work ", "for authentications in subsequent sessions." ), ) .into(), (7, 13) => ( "User Account Disabled", concat!( "Sometimes a system administrator will have to disa", "ble a user's account (e.g., due to lack of payment", ", abuse, evidence of a break-in attempt, etc). Thi", "s error code occurs after a successful authenticat", "ion to a disabled account. This informs the client", " that the failure is permanent until the user cont", "acts their system administrator to get the account", " re-enabled. It differs from a generic authenticat", "ion failure where the client's best option is to p", "resent the passphrase entry dialog in case the use", "r simply mistyped their passphrase." ), ) .into(), (7, 14) => ( "Trust relationship required", concat!( "The submission server requires a configured trust ", "relationship with a third-party server in order to", " access the message content. This value replaces t", "he prior use of X.7.8 for this error condition. th", "ereby updating [RFC4468]." ), ) .into(), (7, 15) => ( "Priority Level is too low", concat!( "The specified priority level is below the lowest p", "riority acceptable for the receiving SMTP server. ", "This condition might be temporary, for example the", " server is operating in a mode where only higher p", "riority messages are accepted for transfer and del", "ivery, while lower priority messages are rejected." ), ) .into(), (7, 16) => ( "Message is too big for the specified priority", concat!( "The message is too big for the specified priority.", " This condition might be temporary, for example th", "e server is operating in a mode where only higher ", "priority messages below certain size are accepted ", "for transfer and delivery." ), ) .into(), (7, 17) => ( "Mailbox owner has changed", concat!( "This status code is returned when a message is rec", "eived with a Require-Recipient-Valid-Since field o", "r RRVS extension and the receiving system is able ", "to determine that the intended recipient mailbox h", "as not been under continuous ownership since the s", "pecified date-time." ), ) .into(), (7, 18) => ( "Domain owner has changed", concat!( "This status code is returned when a message is rec", "eived with a Require-Recipient-Valid-Since field o", "r RRVS extension and the receiving system wishes t", "o disclose that the owner of the domain name of th", "e recipient has changed since the specified date-t", "ime." ), ) .into(), (7, 19) => ( "RRVS test cannot be completed", concat!( "This status code is returned when a message is rec", "eived with a Require-Recipient-Valid-Since field o", "r RRVS extension and the receiving system cannot c", "omplete the requested evaluation because the requi", "red timestamp was not recorded. The message origin", "ator needs to decide whether to reissue the messag", "e without RRVS protection." ), ) .into(), (7, 20) => ( "No passing DKIM signature found", concat!( "This status code is returned when a message did no", "t contain any passing DKIM signatures. (This viola", "tes the advice of Section 6.1 of [RFC6376].)" ), ) .into(), (7, 21) => ( "No acceptable DKIM signature found", concat!( "This status code is returned when a message contai", "ns one or more passing DKIM signatures, but none a", "re acceptable. (This violates the advice of Sectio", "n 6.1 of [RFC6376].)" ), ) .into(), (7, 22) => ( "No valid author-matched DKIM signature found", concat!( "This status code is returned when a message contai", "ns one or more passing DKIM signatures, but none a", "re acceptable because none have an identifier(s) t", "hat matches the author address(es) found in the Fr", "om header field. This is a special case of X.7.21.", " (This violates the advice of Section 6.1 of [RFC6", "376].)" ), ) .into(), (7, 23) => ( "SPF validation failed", concat!( "This status code is returned when a message comple", "ted an SPF check that produced a \"fail\" result, ", "contrary to local policy requirements. Used in pla", "ce of 5.7.1 as described in Section 8.4 of [RFC720", "8]." ), ) .into(), (7, 24) => ( "SPF validation error", concat!( "This status code is returned when evaluation of SP", "F relative to an arriving message resulted in an e", "rror. Used in place of 4.4.3 or 5.5.2 as described", " in Sections 8.6 and 8.7 of [RFC7208]." ), ) .into(), (7, 25) => ( "Reverse DNS validation failed", concat!( "This status code is returned when an SMTP client's", " IP address failed a reverse DNS validation check,", " contrary to local policy requirements." ), ) .into(), (7, 26) => ( "Multiple authentication checks failed", concat!( "This status code is returned when a message failed", " more than one message authentication check, contr", "ary to local policy requirements. The particular m", "echanisms that failed are not specified." ), ) .into(), (7, 27) => ( "Sender address has null MX", concat!( "This status code is returned when the associated s", "ender address has a null MX, and the SMTP receiver", " is configured to reject mail from such sender (e.", "g., because it could not return a DSN)." ), ) .into(), (7, 28) => ( "Mail flood detected", concat!( "The message appears to be part of a mail flood of ", "similar abusive messages." ), ) .into(), (7, 29) => ( "ARC validation failure", concat!( "This status code may be returned when a message fa", "ils ARC validation." ), ) .into(), (7, 30) => ( "REQUIRETLS support required", concat!( "This indicates that the message was not able to be", " forwarded because it was received with a REQUIRET", "LS requirement and none of the SMTP servers to whi", "ch the message should be forwarded provide this su", "pport." ), ) .into(), _ => None, } } } smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/response/mod.rs000066400000000000000000000035241462535561100241270ustar00rootroot00000000000000/* * Copyright (c) 2020-2024, Stalwart Labs Ltd. * * Licensed under the Apache License, Version 2.0 or the MIT license * , at your * option. This file may not be copied, modified, or distributed * except according to those terms. */ use std::fmt::Display; use crate::tokens::define_tokens_128; use crate::{EhloResponse, Response}; pub mod generate; pub mod parser; define_tokens_128! { _8BITMIME = "8BITMIME", ATRN, AUTH, BINARYMIME, BURL, CHECKPOINT, CHUNKING, CONNEG, CONPERM, DELIVERBY, DSN, ENHANCEDSTATUSCO, ETRN, EXPN, VRFY, FUTURERELEASE, HELP, MT_PRIORITY = "MT-PRIORITY", MTRK, NO_SOLICITING = "NO-SOLICITING", ONEX, PIPELINING, REQUIRETLS, RRVS, SIZE, SMTPUTF8, STARTTLS, VERB, // Priorities MIXER, STANAG4406, NSEP, } impl EhloResponse { /// Returns the hostname of the SMTP server. pub fn hostname(&self) -> &T { &self.hostname } /// Returns the capabilities of the SMTP server. pub fn capabilities(&self) -> u32 { self.capabilities } /// Returns `true` if the SMTP server supports a given extension. pub fn has_capability(&self, capability: u32) -> bool { (self.capabilities & capability) != 0 } /// Returns all supported authentication mechanisms. pub fn auth(&self) -> u64 { self.auth_mechanisms } } impl Display for Response { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Code: {}, Enhanced code: {}.{}.{}, Message: {}", self.code, self.esc[0], self.esc[1], self.esc[2], self.message, ) } } smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/response/parser.rs000066400000000000000000000563411462535561100246510ustar00rootroot00000000000000/* * Copyright (c) 2020-2024, Stalwart Labs Ltd. * * Licensed under the Apache License, Version 2.0 or the MIT license * , at your * option. This file may not be copied, modified, or distributed * except according to those terms. */ use std::slice::Iter; use crate::{request::parser::Rfc5321Parser, *}; use super::*; pub const MAX_RESPONSE_LENGTH: usize = 4096; #[derive(Default)] pub struct ResponseReceiver { buf: Vec, code: u16, esc: [u8; 3], esc_cur: [u8; 3], esc_restore: [u8; 10], esc_pos: usize, is_last: bool, pos: usize, } impl ResponseReceiver { pub fn from_code(code: u16) -> Self { Self { code, esc: [0u8; 3], esc_cur: [0u8; 3], esc_restore: [0u8; 10], esc_pos: 0, pos: 3, is_last: false, buf: Vec::new(), } } pub fn parse(&mut self, bytes: &mut Iter<'_, u8>) -> Result, Error> { for &ch in bytes { match self.pos { 0..=2 => { if ch.is_ascii_digit() { if self.buf.is_empty() { self.code = self .code .saturating_mul(10) .saturating_add((ch - b'0') as u16); } self.pos += 1; } else { return Err(Error::SyntaxError { syntax: "Invalid response code", }); } } 3 => match ch { b' ' => { self.is_last = true; self.pos += 1; } b'-' => { self.pos += 1; } b'\r' => { continue; } b'\n' => { self.is_last = true; } _ => { return Err(Error::SyntaxError { syntax: "Invalid response separator", }); } }, 4..=6 => { let mut do_restore = false; match ch { b'0'..=b'9' => { let code = &mut self.esc_cur[self.pos - 4]; if *code < 100 { if let Some(restore) = self.esc_restore.get_mut(self.esc_pos) { *restore = ch; *code = code.saturating_mul(10).saturating_add(ch - b'0'); self.esc_pos += 1; } else { do_restore = true; } } else { do_restore = true; } } b'.' if self.pos < 6 => { if let Some(restore) = self.esc_restore.get_mut(self.esc_pos) { *restore = ch; self.pos += 1; self.esc_pos += 1; } else { do_restore = true; } } b' ' | b'\r' | b'\n' if self.pos == 6 => { self.esc = self.esc_cur; self.esc_cur.fill(0); self.pos = 7; self.esc_pos = 0; } _ => { do_restore = true; } } if do_restore { // ESC parsing failed, restore parsed digits if self.esc_pos > 0 { self.buf .extend_from_slice(&self.esc_restore[..self.esc_pos]); } self.buf.push(ch); self.pos = 7; self.esc_pos = 0; self.esc_cur.fill(0); } } _ => match ch { b'\r' | b'\n' => (), _ => { if self.buf.len() < MAX_RESPONSE_LENGTH { self.buf.push(ch); } else { return Err(Error::ResponseTooLong); } } }, } if ch == b'\n' { if self.is_last { return Ok(Response { code: self.code, esc: self.esc, message: std::mem::take(&mut self.buf).into_string(), }); } else { self.buf.push(b'\n'); self.pos = 0; } } } Err(Error::NeedsMoreData { bytes_left: 0 }) } pub fn reset(&mut self) { self.is_last = false; self.code = 0; self.esc.fill(0); self.esc_cur.fill(0); self.pos = 0; self.esc_pos = 0; self.buf.clear(); } } impl EhloResponse { pub fn parse(bytes: &mut Iter<'_, u8>) -> Result { let mut parser = Rfc5321Parser::new(bytes); let mut response = EhloResponse::default(); let mut eol = false; let mut is_first_line = true; while !eol { let mut code: u16 = 0; for _ in 0..3 { match parser.read_char()? { ch @ b'0'..=b'9' => { code = code.saturating_mul(10).saturating_add((ch - b'0') as u16); } _ => { return Err(Error::SyntaxError { syntax: "unexpected token", }); } } } if code != 250 { return Err(Error::InvalidResponse { code }); } match parser.read_char()? { b' ' => { eol = true; } b'-' => (), b'\n' if code < 600 => { break; } _ => { return Err(Error::SyntaxError { syntax: "unexpected token", }); } } if !is_first_line { response.capabilities |= match parser.hashed_value_long()? { _8BITMIME => EXT_8BIT_MIME, ATRN => EXT_ATRN, AUTH => { while parser.stop_char != LF { if let Some(mechanism) = parser.mechanism()? { response.auth_mechanisms |= mechanism; } } EXT_AUTH } BINARYMIME => EXT_BINARY_MIME, BURL => EXT_BURL, CHECKPOINT => EXT_CHECKPOINT, CHUNKING => EXT_CHUNKING, CONNEG => EXT_CONNEG, CONPERM => EXT_CONPERM, DELIVERBY => { response.deliver_by = if parser.stop_char != LF { let db = parser.size()?; if db != usize::MAX { db as u64 } else { 0 } } else { 0 }; EXT_DELIVER_BY } DSN => EXT_DSN, ENHANCEDSTATUSCO if parser.stop_char.to_ascii_uppercase() == b'D' && parser.read_char()?.to_ascii_uppercase() == b'E' && parser.read_char()?.to_ascii_uppercase() == b'S' => { EXT_ENHANCED_STATUS_CODES } ETRN => EXT_ETRN, EXPN => EXT_EXPN, VRFY => EXT_VRFY, FUTURERELEASE => { let max_interval = if parser.stop_char != LF { parser.size()? } else { 0 }; let max_datetime = if parser.stop_char != LF { parser.size()? } else { 0 }; response.future_release_interval = if max_interval != usize::MAX { max_interval as u64 } else { 0 }; response.future_release_datetime = if max_datetime != usize::MAX { max_datetime as u64 } else { 0 }; EXT_FUTURE_RELEASE } HELP => EXT_HELP, MT_PRIORITY => { response.mt_priority = if parser.stop_char != LF { match parser.hashed_value_long()? { MIXER => MtPriority::Mixer, STANAG4406 => MtPriority::Stanag4406, NSEP => MtPriority::Nsep, _ => MtPriority::Mixer, } } else { MtPriority::Mixer }; EXT_MT_PRIORITY } MTRK => EXT_MTRK, NO_SOLICITING => { response.no_soliciting = if parser.stop_char != LF { let text = parser.text()?; if !text.is_empty() { text.into() } else { None } } else { None }; EXT_NO_SOLICITING } ONEX => EXT_ONEX, PIPELINING => EXT_PIPELINING, REQUIRETLS => EXT_REQUIRE_TLS, RRVS => EXT_RRVS, SIZE => { response.size = if parser.stop_char != LF { let size = parser.size()?; if size != usize::MAX { size } else { 0 } } else { 0 }; EXT_SIZE } SMTPUTF8 => EXT_SMTP_UTF8, STARTTLS => EXT_START_TLS, VERB => EXT_VERB, _ => 0, }; parser.seek_lf()?; } else { let mut buf = Vec::with_capacity(16); loop { match parser.read_char()? { b'\n' => break, b'\r' => (), b' ' => { parser.seek_lf()?; break; } ch if buf.len() < MAX_RESPONSE_LENGTH => { buf.push(ch); } _ => return Err(Error::ResponseTooLong), } } is_first_line = false; response.hostname = buf.into_string(); } } Ok(response) } } #[cfg(test)] mod tests { use crate::*; use super::ResponseReceiver; #[test] fn parse_ehlo() { for item in [ ( concat!( "250-dbc.mtview.ca.us says hello\n", "250-8BITMIME\n", "250-ATRN\n", "250-AUTH GSSAPI DIGEST-MD5 PLAIN\n", "250-BINARYMIME\n", "250-BURL imap\n", "250-CHECKPOINT\n", "250-CHUNKING\n", "250-CONNEG\n", "250-CONPERM\n", "250-DELIVERBY\n", "250-DSN\n", "250-ENHANCEDSTATUSCODES\n", "250-ETRN\n", "250-EXPN\n", "250-VRFY\n", "250-FUTURERELEASE 1234 5678\n", "250-HELP\n", "250-MT-PRIORITY\n", "250-MTRK\n", "250-NO-SOLICITING net.example:ADV\n", "250-PIPELINING\n", "250-REQUIRETLS\n", "250-RRVS\n", "250-SIZE 1000000\n", "250-SMTPUTF8 ignore\n", "250 STARTTLS\n", ), Ok(EhloResponse { hostname: "dbc.mtview.ca.us".to_string(), capabilities: EXT_8BIT_MIME | EXT_ATRN | EXT_AUTH | EXT_BINARY_MIME | EXT_BURL | EXT_CHECKPOINT | EXT_CHUNKING | EXT_CONNEG | EXT_CONPERM | EXT_DELIVER_BY | EXT_DSN | EXT_ENHANCED_STATUS_CODES | EXT_ETRN | EXT_EXPN | EXT_VRFY | EXT_FUTURE_RELEASE | EXT_HELP | EXT_MT_PRIORITY | EXT_MTRK | EXT_NO_SOLICITING | EXT_PIPELINING | EXT_REQUIRE_TLS | EXT_RRVS | EXT_SIZE | EXT_SMTP_UTF8 | EXT_START_TLS, auth_mechanisms: AUTH_GSSAPI | AUTH_DIGEST_MD5 | AUTH_PLAIN, deliver_by: 0, future_release_interval: 1234, future_release_datetime: 5678, mt_priority: MtPriority::Mixer, no_soliciting: Some("net.example:ADV".to_string()), size: 1000000, }), ), ( concat!( "250-\n", "250-DELIVERBY 240\n", "250-FUTURERELEASE 123\n", "250-MT-PRIORITY MIXER\n", "250-NO-SOLICITING\n", "250-SIZE\n", "250 SMTPUTF8\n", ), Ok(EhloResponse { hostname: "".to_string(), capabilities: EXT_DELIVER_BY | EXT_FUTURE_RELEASE | EXT_MT_PRIORITY | EXT_NO_SOLICITING | EXT_SIZE | EXT_SMTP_UTF8, auth_mechanisms: 0, deliver_by: 240, future_release_interval: 123, future_release_datetime: 0, mt_priority: MtPriority::Mixer, no_soliciting: None, size: 0, }), ), ( concat!( "250-dbc.mtview.ca.us says hello\n", "250-FUTURERELEASE\n", "250 MT-PRIORITY STANAG4406\n", ), Ok(EhloResponse { hostname: "dbc.mtview.ca.us".to_string(), capabilities: EXT_FUTURE_RELEASE | EXT_MT_PRIORITY, auth_mechanisms: 0, deliver_by: 0, future_release_interval: 0, future_release_datetime: 0, mt_priority: MtPriority::Stanag4406, no_soliciting: None, size: 0, }), ), ( concat!("523-Massive\n", "523-Error\n", "523 Message\n"), Err(Error::InvalidResponse { code: 523 }), ), ] { let (response, parsed_response): (&str, Result, Error>) = item; for replacement in ["", "\r\n", " \n", " \r\n"] { let response = if !replacement.is_empty() && parsed_response.is_ok() { response.replace('\n', replacement) } else { response.to_string() }; assert_eq!( parsed_response, EhloResponse::parse(&mut response.as_bytes().iter()), "failed for {response:?}", ); } } } #[test] fn parse_response() { let mut all_responses = Vec::new(); let mut all_parsed_responses = Vec::new(); for (response, parsed_response, _) in [ ( "250 2.1.1 Originator ok\n", Response { code: 250, esc: [2, 1, 1], message: "Originator ok".to_string(), }, true, ), ( concat!( "551-5.7.1 Forwarding to remote hosts disabled\n", "551 5.7.1 Select another host to act as your forwarder\n" ), Response { code: 551, esc: [5, 7, 1], message: concat!( "Forwarding to remote hosts disabled\n", "Select another host to act as your forwarder" ) .to_string(), }, true, ), ( concat!( "550-mailbox unavailable\n", "550 user has moved with no forwarding address\n" ), Response { code: 550, esc: [0, 0, 0], message: "mailbox unavailable\nuser has moved with no forwarding address" .to_string(), }, false, ), ( concat!( "550-mailbox unavailable\n", "550 user has moved with no forwarding address\n" ), Response { code: 550, esc: [0, 0, 0], message: "mailbox unavailable\nuser has moved with no forwarding address" .to_string(), }, true, ), ( concat!( "432-6.8.9\n", "432-6.8.9 Hello\n", "432-6.8.9 \n", "432-6.8.9 ,\n", "432-\n", "432-6\n", "432-6.\n", "432-6.8\n", "432-6.8.9\n", "432 6.8.9 World!\n" ), Response { code: 432, esc: [6, 8, 9], message: "\nHello\n\n,\n\n\n6\n\n6.\n\n6.8\n\n\nWorld!".to_string(), }, true, ), ( concat!("250 2address.org\n"), Response { code: 250, esc: [0, 0, 0], message: "2address.org".to_string(), }, true, ), ( concat!("250 100.address.org\n"), Response { code: 250, esc: [0, 0, 0], message: "100.address.org".to_string(), }, true, ), ( concat!("250 111111111111\n"), Response { code: 250, esc: [0, 0, 0], message: "111111111111".to_string(), }, true, ), ( concat!("250 99999999999999\n"), Response { code: 250, esc: [0, 0, 0], message: "99999999999999".to_string(), }, true, ), ( concat!("250 2.0.0 Message queued for delivery.\r\n"), Response { code: 250, esc: [2, 0, 0], message: "Message queued for delivery.".to_string(), }, true, ), ( concat!("250-Missing space\n", "250\n", "250 Ignore this"), Response { code: 250, esc: [0, 0, 0], message: "Missing space\n".to_string(), }, true, ), ] { assert_eq!( parsed_response, ResponseReceiver::default() .parse(&mut response.as_bytes().iter()) .unwrap(), "failed for {response:?}", ); all_responses.extend_from_slice(response.as_bytes()); all_parsed_responses.push(parsed_response); } // Test receiver for chunk_size in [5, 10, 20, 30, 40, 50, 60] { let mut receiver = ResponseReceiver::default(); let mut parsed_response = all_parsed_responses.clone().into_iter(); for chunk in all_responses.chunks(chunk_size) { let mut bytes = chunk.iter(); loop { match receiver.parse(&mut bytes) { Ok(response) => { assert_eq!( parsed_response.next(), Some(response), "chunk size {chunk_size}", ); receiver.reset(); } Err(Error::NeedsMoreData { .. }) => { break; } err => panic!("Unexpected error {err:?} for chunk size {chunk_size}"), } } } } } } smtp-proto-f4d727091591bd0689a06c35913abe12b461839c/src/tokens.rs000066400000000000000000000024401462535561100230110ustar00rootroot00000000000000/* * Copyright (c) 2020-2024, Stalwart Labs Ltd. * * Licensed under the Apache License, Version 2.0 or the MIT license * , at your * option. This file may not be copied, modified, or distributed * except according to those terms. */ const fn str_to_array(s: &str) -> [u8; N] { let s = s.as_bytes(); let mut arr = [0; N]; let mut i = 0; while i < s.len() { arr[i] = s[i]; i += 1; } arr } pub(crate) const fn token64(s: &str) -> u64 { u64::from_le_bytes(str_to_array(s)) } pub(crate) const fn token128(s: &str) -> u128 { u128::from_le_bytes(str_to_array(s)) } macro_rules! first { ($head:expr$(, $tail:expr)*) => { $head }; } macro_rules! define_tokens_64 { ($($a:ident $(= $b:expr)?,)*) => { $(pub(crate) const $a: u64 = $crate::tokens::token64($crate::tokens::first!($($b,)* stringify!($a)));)* }; } macro_rules! define_tokens_128 { ($($a:ident $(= $b:expr)?,)*) => { $(pub(crate) const $a: u128 = $crate::tokens::token128($crate::tokens::first!($($b,)* stringify!($a)));)* }; } pub(crate) use define_tokens_128; pub(crate) use define_tokens_64; pub(crate) use first;