From 001dfb68238a2673fdddd520a434cb2c94617386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 5 Mar 2026 21:21:06 +0100 Subject: [PATCH 1/2] build: copy config from imap-codec --- .github/FUNDING.yml | 1 + .github/actions/cache_restore/action.yml | 21 +++ .github/actions/cache_save/action.yml | 18 ++ .github/dependabot.yml | 14 ++ .github/workflows/audit.yml | 26 +++ .github/workflows/main.yml | 95 ++++++++++ .github/workflows/release.yml | 42 +++++ .gitignore | 9 +- CHANGELOG.md | 33 ++++ CONTRIBUTING.md | 30 ++++ Cargo.lock | 217 +++++++++++++++++++++++ Cargo.toml | 7 +- README.md | 53 +++++- deny.toml | 15 +- flake.lock | 27 +++ flake.nix | 36 ++++ justfile | 217 +++++++++++++++++++++++ rust-toolchain.toml | 4 + rustfmt.toml | 3 + shell.nix | 13 ++ 20 files changed, 861 insertions(+), 20 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/actions/cache_restore/action.yml create mode 100644 .github/actions/cache_save/action.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/audit.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 justfile create mode 100644 rust-toolchain.toml create mode 100644 rustfmt.toml create mode 100644 shell.nix diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1c9bc12 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [duesee] diff --git a/.github/actions/cache_restore/action.yml b/.github/actions/cache_restore/action.yml new file mode 100644 index 0000000..6534e60 --- /dev/null +++ b/.github/actions/cache_restore/action.yml @@ -0,0 +1,21 @@ +name: cache_restore +runs: + using: composite + steps: + - uses: actions/cache/restore@v4 + with: + path: | + # See https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + # See https://doc.rust-lang.org/cargo/guide/build-cache.html + target + key: ${{ runner.os }}|${{ github.job }}|${{ github.run_attempt }} + restore-keys: | + ${{ runner.os }}|${{ github.job }} + ${{ runner.os }} + diff --git a/.github/actions/cache_save/action.yml b/.github/actions/cache_save/action.yml new file mode 100644 index 0000000..3846bf2 --- /dev/null +++ b/.github/actions/cache_save/action.yml @@ -0,0 +1,18 @@ +name: cache_save +runs: + using: composite + steps: + - uses: actions/cache/save@v4 + with: + path: | + # See https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + # See https://doc.rust-lang.org/cargo/guide/build-cache.html + target + key: ${{ runner.os }}|${{ github.job }}|${{ github.run_attempt }} + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3c0f0f2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + groups: + dependencies: + patterns: + - "*" diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000..0765e83 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,26 @@ +name: audit +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + # 21:43 on Wednesday and Sunday. (Thanks, crontab.guru) + - cron: '43 21 * * 3,0' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + audit: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just audit + - uses: ./.github/actions/cache_save diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..94825f4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,95 @@ +name: main +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just check + - uses: ./.github/actions/cache_save + + test: + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just test + - uses: ./.github/actions/cache_save + +# benchmark: +# runs-on: ubuntu-latest + +# steps: +# - uses: actions/checkout@v6 + +# - uses: ./.github/actions/cache_restore +# - run: cargo install just +# - run: just bench_against_main +# - uses: ./.github/actions/cache_save + + coverage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just coverage + - uses: ./.github/actions/cache_save + + - uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e + with: + format: lcov + file: target/coverage/coverage.lcov + + fuzz: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just fuzz + - uses: ./.github/actions/cache_save + + check_msrv: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just check_msrv + - uses: ./.github/actions/cache_save + + check_minimal_dependency_versions: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just check_minimal_dependency_versions + - uses: ./.github/actions/cache_save + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..92901ee --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: release + +on: + push: + tags: + - 'imap-codec/v*' + - 'imap-types/v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Extract crate name from Git tag + run: | + set -euo pipefail + tag_name=${GITHUB_REF#refs/tags/} + crate_name=${tag_name%/v*} + echo "Extracted crate name: $crate_name" + echo "CRATE_NAME=$crate_name" >> "$GITHUB_ENV" + + - uses: actions/checkout@v6 + + - name: Assert release version matches crate version + run: | + set -euo pipefail + + # Get release version from Git tag + tag_version=${GITHUB_REF#refs/tags/$CRATE_NAME/v} + + # Get crate version from Cargo.toml + cd $CRATE_NAME + crate_version=$(cargo read-manifest | jq -r .version) + + if [ "$tag_version" != "$crate_version" ]; then + echo "Error: Release version in Git tag (${tag_version}) does not match crate version in Cargo.toml (${crate_version}) for crate $CRATE_NAME." + exit 1 + fi + + - name: Publish crate to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p $CRATE_NAME diff --git a/.gitignore b/.gitignore index 80cb495..9cfdee9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ -Cargo.lock -/target +target .idea + +# direnv (https://direnv.net/) +.envrc +.direnv + +nohup.out diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1c3d243 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] - YYYY-MM-DD + +### Added + +* Initial implementation of smtp-codec and smtp-types +* Support for RFC 5321 SMTP protocol +* Commands: EHLO, HELO, MAIL, RCPT, DATA, RSET, QUIT, NOOP, VRFY, EXPN, HELP +* Responses: Single-line and multi-line responses with reply codes +* Greeting parsing (220 response) +* EHLO response parsing with capability extraction + +### SMTP Extensions + +* `starttls` - STARTTLS command (RFC 3207) +* `ext_auth` - SMTP Authentication (RFC 4954) +* `ext_size` - Message Size Declaration (RFC 1870) +* `ext_8bitmime` - 8-bit MIME Transport (RFC 6152) +* `ext_pipelining` - Command Pipelining (RFC 2920) +* `ext_smtputf8` - Internationalized Email (RFC 6531) +* `ext_enhancedstatuscodes` - Enhanced Error Codes (RFC 2034) + +### Additional Features + +* `quirk_crlf_relaxed` - Accept LF without preceding CR +* `arbitrary` - Derive Arbitrary for fuzzing +* `serde` - Serialize/Deserialize support diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3856be8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Welcome to smtp-codec's (and smtp-types') contributing guide + +Thanks for investing your time to help with this project! Keep in mind that this project is driven by volunteers. Be patient and polite, and empower others to improve. Always use your best judgment and be excellent to each other. + +## Principles + +### Misuse resistance + +We use strong-typing to eliminate invalid state. +Ask yourself: Can I instantiate a type with an invalid variable setting? +If yes, consider how to eliminate it. +If you're unsure, let's figure it out together! + +## Project management + +We use the [just](https://github.com/casey/just) command runner for Continuous Integration (CI). +The GitHub Actions infrastructure merely calls `just` to execute jobs. +This means that you can run all required tests for a PR using `just ci`. + +### Code formatting + +Please ensure that all code is formatted using `cargo +nightly fmt`. + +### Testing + +Run tests with `cargo test --all-features`. + +## License + +By contributing to this project, you agree to license your contributions under the same license as the project (MIT OR Apache-2.0). diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..01bade9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,217 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "abnf-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182d1f071b906a9f59269c89af101515a5cbe58f723eb6717e7fe7445c0dea" +dependencies = [ + "nom", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bounded-static" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beb903daa49b43bcafb5d5eebe633f9ad638d8b16cd08f95fb05ee7bd099321" + +[[package]] +name = "bounded-static-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0af050e27e5d57aa14975f97fe47a134c46a390f91819f23a625319a7111bfa" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "smtp-codec" +version = "0.3.0" +dependencies = [ + "abnf-core", + "base64", + "log", + "nom", + "smtp-types", +] + +[[package]] +name = "smtp-types" +version = "0.3.0" +dependencies = [ + "arbitrary", + "base64", + "bounded-static", + "bounded-static-derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index c4872be..66ca435 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,9 @@ [workspace] resolver = "2" members = [ - "smtp-types", "smtp-codec", + "smtp-types", ] -[patch.crates-io] -smtp-types = { path = "smtp-types" } -smtp-codec = { path = "smtp-codec" } +[workspace.package] +rust-version = "1.85" diff --git a/README.md b/README.md index c028e43..160e19a 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,48 @@ -[![CI](https://github.com/duesee/smtp-codec/actions/workflows/ci.yml/badge.svg)](https://github.com/duesee/smtp-codec/actions/workflows/ci.yml) -[![Scheduled](https://github.com/duesee/smtp-codec/actions/workflows/scheduled.yml/badge.svg)](https://github.com/duesee/smtp-codec/actions/workflows/scheduled.yml) -[![docs](https://docs.rs/smtp-codec/badge.svg)](https://docs.rs/smtp-codec) +# smtp-{codec,types} -# smtp-codec +This workspace contains `smtp-codec` and `smtp-types`, two crates for building SMTP clients and servers following [RFC 5321]. -This library provides parsing, serialization, and support for SMTP implementations. -It is based on `smtp-types` and aims to become a rock-solid building block for SMTP client and server implementations in Rust. -Many things are there, but `smtp-codec` is still in a rough state. +`smtp-codec` provides parsing and serialization, and is based on `smtp-types`. +`smtp-types` provides misuse-resistant types, constructors, and general support for SMTP implementations. -# License +## Features -This crate is dual-licensed under Apache 2.0 and MIT terms. +* Complete formal syntax of SMTP is implemented following RFC 5321 +* Several SMTP extensions are supported (AUTH, SIZE, 8BITMIME, PIPELINING, STARTTLS, SMTPUTF8, ENHANCEDSTATUSCODES) +* Correctness and misuse-resistance are enforced on the type level +* Comprehensive test coverage + +## Usage + +```rust +use smtp_codec::{decode::Decoder, encode::Encoder, CommandCodec, GreetingCodec}; + +// Parse a greeting +let input = b"220 mail.example.com ESMTP ready\r\n"; +let (remaining, greeting) = GreetingCodec::default().decode(input).unwrap(); +println!("Domain: {}", greeting.domain); + +// Encode a command +use smtp_codec::smtp_types::command::Command; +let cmd = Command::quit(); +let bytes = CommandCodec::default().encode(&cmd); +assert_eq!(bytes.dump(), b"QUIT\r\n"); +``` + +## Supported Extensions + +| Feature | Description | RFC | +|-------------------------|------------------------------------------|----------| +| starttls | SMTP over TLS | RFC 3207 | +| ext_auth | SMTP Authentication | RFC 4954 | +| ext_size | Message Size Declaration | RFC 1870 | +| ext_8bitmime | 8-bit MIME Transport | RFC 6152 | +| ext_pipelining | Command Pipelining | RFC 2920 | +| ext_smtputf8 | Internationalized Email | RFC 6531 | +| ext_enhancedstatuscodes | Enhanced Error Codes | RFC 2034 | + +## License + +Licensed under either of Apache License, Version 2.0 or MIT license at your option. + +[RFC 5321]: https://tools.ietf.org/html/rfc5321 diff --git a/deny.toml b/deny.toml index 9a95fcf..b4e095d 100644 --- a/deny.toml +++ b/deny.toml @@ -1,9 +1,14 @@ -[bans] -multiple-versions = "deny" - [sources] unknown-registry = "deny" -unknown-git = "deny" +unknown-git = "deny" [licenses] -allow = [ "Apache-2.0", "MIT", "Unicode-DFS-2016" ] +allow = [ + "Apache-2.0", + "MIT", + "NCSA", + "Unicode-3.0" +] + +[licenses.private] +ignore = true diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6f01dd9 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1757023789, + "narHash": "sha256-roMtzAgp0M4ExsIsFScWvWY0t1vjWtJwsqaxFQ2hwk8=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d689442f4e5a79df371c65b977ffff66d8fed809", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.05-small", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..047a0ee --- /dev/null +++ b/flake.nix @@ -0,0 +1,36 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05-small"; + }; + + outputs = + { self, nixpkgs, ... }: + let + eachSupportedSystem = nixpkgs.lib.genAttrs supportedSystems; + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + mkDevShells = + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + default = pkgs.mkShell { + strictDeps = true; + nativeBuildInputs = with pkgs; [ + just + rustPlatform.bindgenHook + rustup + ]; + }; + }; + in + { + devShells = eachSupportedSystem mkDevShells; + }; +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..e6d797d --- /dev/null +++ b/justfile @@ -0,0 +1,217 @@ +export RUSTFLAGS := "-D warnings" +export RUSTDOCFLAGS := "-D warnings" + +msrv := `sed -rn 's|^rust-version = \"(.*)\"$|\1|p' Cargo.toml` + +[private] +default: + just -l --unsorted + +########### +### RUN ### +########### + +# Run (local) CI +ci: (ci_impl "" "" ) \ + (ci_impl "" " --all-features") \ + (ci_impl " --release" "" ) \ + (ci_impl " --release" " --all-features") + +[private] +ci_impl mode features: (check_impl mode features) (test_impl mode features) + +# Check syntax, formatting, clippy, deny, semver, ... +check: (check_impl "" "" ) \ + (check_impl "" " --all-features") \ + (check_impl " --release" "" ) \ + (check_impl " --release" " --all-features") + +[private] +check_impl mode features: (cargo_check mode features) \ + (cargo_hack mode) \ + cargo_fmt \ + (cargo_clippy mode features) \ + cargo_deny \ + cargo_semver + +[private] +cargo_check mode features: + cargo check --workspace --all-targets{{ mode }}{{ features }} + cargo doc --no-deps --document-private-items --keep-going{{ mode }}{{ features }} + +[private] +cargo_hack mode: install_cargo_hack + cargo hack check --workspace --all-targets{{ mode }} + cargo hack check -p smtp-codec \ + --no-dev-deps \ + --exclude-features default \ + --feature-powerset \ + --group-features \ + arbitrary,\ + serde \ + --group-features \ + starttls,\ + ext_auth,\ + ext_size,\ + ext_8bitmime,\ + ext_pipelining,\ + ext_smtputf8,\ + ext_enhancedstatuscodes,\ + quirk_crlf_relaxed\ + {{ mode }} + cargo hack check -p smtp-types \ + --no-dev-deps \ + --feature-powerset \ + --group-features \ + arbitrary,\ + serde \ + --group-features \ + starttls,\ + ext_auth,\ + ext_size,\ + ext_8bitmime,\ + ext_pipelining,\ + ext_smtputf8,\ + ext_enhancedstatuscodes\ + {{ mode }} + +[private] +cargo_fmt: install_rust_nightly install_rust_nightly_fmt + cargo +nightly fmt --check + +[private] +cargo_clippy features mode: install_cargo_clippy + cargo clippy --workspace --all-targets{{ features }}{{ mode }} + +[private] +cargo_deny: install_cargo_deny + cargo deny check + +[private] +cargo_semver: install_cargo_semver_checks + cargo semver-checks check-release --only-explicit-features --baseline-rev HEAD -p smtp-codec + cargo semver-checks check-release --only-explicit-features --baseline-rev HEAD -p smtp-types + +# Test multiple configurations +test: (test_impl "" "" ) \ + (test_impl "" " --all-features") \ + (test_impl " --release" "" ) \ + (test_impl " --release" " --all-features") + +[private] +test_impl mode features: (cargo_test mode features) + +[private] +cargo_test features mode: + cargo test \ + --workspace \ + --all-targets \ + {{ features }}\ + {{ mode }} + +# Audit advisories, bans, licenses, and sources +audit: cargo_deny + +# Measure test coverage +coverage: install_rust_llvm_tools_preview install_cargo_grcov + rm -rf target/coverage/* + RUSTFLAGS="-Cinstrument-coverage" LLVM_PROFILE_FILE="$PWD/target/coverage/coverage-%m-%p.profraw" CARGO_TARGET_DIR="$PWD/target/coverage" cargo test -p smtp-codec -p smtp-types --all-features + grcov target/coverage \ + --source-dir . \ + --binary-path target/coverage/debug \ + --branch \ + --keep-only '{smtp-codec/src/**,smtp-types/src/**}' \ + --llvm \ + --output-types "html,lcov" \ + --output-path target/coverage/ + mv target/coverage/lcov target/coverage/coverage.lcov + rm target/coverage/*.profraw + rm -rf target/coverage/debug + +# Fuzz all targets +[linux] +fuzz runs="25000": install_cargo_fuzz + #!/usr/bin/env bash + set -euo pipefail + cd smtp-codec + for fuzz_target in $(cargo +nightly fuzz list) + do + echo "# Fuzzing ${fuzz_target}"; + cargo +nightly fuzz run --features=ext ${fuzz_target} -- -dict=fuzz/terminals.dict -max_len=256 -only_ascii=1 -runs={{ runs }}; + done + +# Check MSRV +check_msrv: install_rust_msrv + cargo '+{{ msrv }}' check --locked \ + --workspace \ + --all-targets --all-features + cargo '+{{ msrv }}' test --locked \ + --workspace \ + --all-targets --all-features + +# Check minimal dependency versions +check_minimal_dependency_versions: install_rust_nightly + cargo +nightly update -Z minimal-versions + cargo check \ + --workspace \ + --all-targets --all-features + cargo test \ + --workspace \ + --all-targets --all-features + cargo update + +############### +### INSTALL ### +############### + +# Install required tooling (ahead of time) +install: install_rust_msrv \ + install_rust_nightly \ + install_rust_nightly_fmt \ + install_rust_llvm_tools_preview \ + install_cargo_clippy \ + install_cargo_deny \ + install_cargo_fuzz \ + install_cargo_grcov \ + install_cargo_hack \ + install_cargo_semver_checks + +[private] +install_rust_msrv: + rustup toolchain install '{{ msrv }}' --profile minimal + +[private] +install_rust_nightly: + rustup toolchain install nightly --profile minimal + +[private] +install_rust_nightly_fmt: + rustup component add --toolchain nightly rustfmt + +[private] +install_rust_llvm_tools_preview: + rustup component add llvm-tools-preview + +[private] +install_cargo_clippy: + rustup component add clippy + +[private] +install_cargo_deny: + cargo install --locked cargo-deny + +[private] +install_cargo_grcov: + cargo install grcov + +[private] +install_cargo_hack: + cargo install --locked cargo-hack + +[private] +install_cargo_fuzz: install_rust_nightly + cargo install cargo-fuzz + +[private] +install_cargo_semver_checks: + cargo install --locked cargo-semver-checks diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..3ccec48 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +profile = "default" +components = [ "rust-src", "rust-analyzer" ] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..2cbf0d3 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +format_code_in_doc_comments=true +group_imports="StdExternalCrate" +imports_granularity="Crate" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..9d07c80 --- /dev/null +++ b/shell.nix @@ -0,0 +1,13 @@ +# Compatiblity file for non-flake Nix users. +# +# +(import + ( + let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in + fetchTarball { + url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { src = ./.; } +).shellNix From 86113253a3669f7b0434cc856af284f1ce4e8e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 7 Mar 2026 02:17:01 +0100 Subject: [PATCH 2/2] docs: remove generated docs --- .github/workflows/release.yml | 4 +- CHANGELOG.md | 33 ---------- Cargo.lock | 110 ++-------------------------------- README.md | 53 +++------------- deny.toml | 2 - justfile | 23 +------ 6 files changed, 16 insertions(+), 209 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92901ee..788c8f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,8 +3,8 @@ name: release on: push: tags: - - 'imap-codec/v*' - - 'imap-types/v*' + - 'smtp-codec/v*' + - 'smtp-types/v*' jobs: release: diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 1c3d243..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,33 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - YYYY-MM-DD - -### Added - -* Initial implementation of smtp-codec and smtp-types -* Support for RFC 5321 SMTP protocol -* Commands: EHLO, HELO, MAIL, RCPT, DATA, RSET, QUIT, NOOP, VRFY, EXPN, HELP -* Responses: Single-line and multi-line responses with reply codes -* Greeting parsing (220 response) -* EHLO response parsing with capability extraction - -### SMTP Extensions - -* `starttls` - STARTTLS command (RFC 3207) -* `ext_auth` - SMTP Authentication (RFC 4954) -* `ext_size` - Message Size Declaration (RFC 1870) -* `ext_8bitmime` - 8-bit MIME Transport (RFC 6152) -* `ext_pipelining` - Command Pipelining (RFC 2920) -* `ext_smtputf8` - Internationalized Email (RFC 6531) -* `ext_enhancedstatuscodes` - Enhanced Error Codes (RFC 2034) - -### Additional Features - -* `quirk_crlf_relaxed` - Accept LF without preceding CR -* `arbitrary` - Derive Arbitrary for fuzzing -* `serde` - Serialize/Deserialize support diff --git a/Cargo.lock b/Cargo.lock index 01bade9..7850e53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,68 +4,13 @@ version = 4 [[package]] name = "abnf-core" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182d1f071b906a9f59269c89af101515a5cbe58f723eb6717e7fe7445c0dea" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" dependencies = [ "nom", ] -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bounded-static" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beb903daa49b43bcafb5d5eebe633f9ad638d8b16cd08f95fb05ee7bd099321" - -[[package]] -name = "bounded-static-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0af050e27e5d57aa14975f97fe47a134c46a390f91819f23a625319a7111bfa" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - [[package]] name = "memchr" version = "2.7.5" @@ -136,41 +81,20 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - [[package]] name = "smtp-codec" -version = "0.3.0" +version = "0.2.0" dependencies = [ "abnf-core", - "base64", - "log", "nom", "smtp-types", ] [[package]] name = "smtp-types" -version = "0.3.0" +version = "0.2.0" dependencies = [ - "arbitrary", - "base64", - "bounded-static", - "bounded-static-derive", "serde", - "serde_json", - "thiserror", ] [[package]] @@ -184,34 +108,8 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/README.md b/README.md index 160e19a..c028e43 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,13 @@ -# smtp-{codec,types} +[![CI](https://github.com/duesee/smtp-codec/actions/workflows/ci.yml/badge.svg)](https://github.com/duesee/smtp-codec/actions/workflows/ci.yml) +[![Scheduled](https://github.com/duesee/smtp-codec/actions/workflows/scheduled.yml/badge.svg)](https://github.com/duesee/smtp-codec/actions/workflows/scheduled.yml) +[![docs](https://docs.rs/smtp-codec/badge.svg)](https://docs.rs/smtp-codec) -This workspace contains `smtp-codec` and `smtp-types`, two crates for building SMTP clients and servers following [RFC 5321]. +# smtp-codec -`smtp-codec` provides parsing and serialization, and is based on `smtp-types`. -`smtp-types` provides misuse-resistant types, constructors, and general support for SMTP implementations. +This library provides parsing, serialization, and support for SMTP implementations. +It is based on `smtp-types` and aims to become a rock-solid building block for SMTP client and server implementations in Rust. +Many things are there, but `smtp-codec` is still in a rough state. -## Features +# License -* Complete formal syntax of SMTP is implemented following RFC 5321 -* Several SMTP extensions are supported (AUTH, SIZE, 8BITMIME, PIPELINING, STARTTLS, SMTPUTF8, ENHANCEDSTATUSCODES) -* Correctness and misuse-resistance are enforced on the type level -* Comprehensive test coverage - -## Usage - -```rust -use smtp_codec::{decode::Decoder, encode::Encoder, CommandCodec, GreetingCodec}; - -// Parse a greeting -let input = b"220 mail.example.com ESMTP ready\r\n"; -let (remaining, greeting) = GreetingCodec::default().decode(input).unwrap(); -println!("Domain: {}", greeting.domain); - -// Encode a command -use smtp_codec::smtp_types::command::Command; -let cmd = Command::quit(); -let bytes = CommandCodec::default().encode(&cmd); -assert_eq!(bytes.dump(), b"QUIT\r\n"); -``` - -## Supported Extensions - -| Feature | Description | RFC | -|-------------------------|------------------------------------------|----------| -| starttls | SMTP over TLS | RFC 3207 | -| ext_auth | SMTP Authentication | RFC 4954 | -| ext_size | Message Size Declaration | RFC 1870 | -| ext_8bitmime | 8-bit MIME Transport | RFC 6152 | -| ext_pipelining | Command Pipelining | RFC 2920 | -| ext_smtputf8 | Internationalized Email | RFC 6531 | -| ext_enhancedstatuscodes | Enhanced Error Codes | RFC 2034 | - -## License - -Licensed under either of Apache License, Version 2.0 or MIT license at your option. - -[RFC 5321]: https://tools.ietf.org/html/rfc5321 +This crate is dual-licensed under Apache 2.0 and MIT terms. diff --git a/deny.toml b/deny.toml index b4e095d..52276c2 100644 --- a/deny.toml +++ b/deny.toml @@ -6,8 +6,6 @@ unknown-git = "deny" allow = [ "Apache-2.0", "MIT", - "NCSA", - "Unicode-3.0" ] [licenses.private] diff --git a/justfile b/justfile index e6d797d..f488757 100644 --- a/justfile +++ b/justfile @@ -46,33 +46,12 @@ cargo_hack mode: install_cargo_hack --no-dev-deps \ --exclude-features default \ --feature-powerset \ - --group-features \ - arbitrary,\ - serde \ - --group-features \ - starttls,\ - ext_auth,\ - ext_size,\ - ext_8bitmime,\ - ext_pipelining,\ - ext_smtputf8,\ - ext_enhancedstatuscodes,\ - quirk_crlf_relaxed\ {{ mode }} cargo hack check -p smtp-types \ --no-dev-deps \ --feature-powerset \ --group-features \ - arbitrary,\ - serde \ - --group-features \ - starttls,\ - ext_auth,\ - ext_size,\ - ext_8bitmime,\ - ext_pipelining,\ - ext_smtputf8,\ - ext_enhancedstatuscodes\ + serde\ {{ mode }} [private]