email_address-0.2.9/.cargo_vcs_info.json0000644000000001360000000000100136400ustar { "git": { "sha1": "59b64fa29a2ab701bf032a818d6ad7cf356fe024" }, "path_in_vcs": "" }email_address-0.2.9/.github/ISSUE_TEMPLATE/bug_report.md000064400000000000000000000014121046102023000206430ustar 00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- ## Describe the bug A clear and concise description of what the bug is. A clear and concise description of what you expected to happen. ## Minimal Reproducible Examples (MRE) Please try to provide information which will help us to fix the issue faster. MREs with few dependencies are especially lovely <3. If applicable, add logs/screenshots to help explain your problem. **Environment (please complete the following information):** - Platform: [e.g.`uname -a`] - Rust [e.g.`rustic -vV`] - Cargo [e.g.`cargo -vV`] ## Additional context Add any other context about the problem here. For example, environment variables like `CARGO`, `RUSTUP_HOME` or `CARGO_HOME`. email_address-0.2.9/.github/ISSUE_TEMPLATE/feature_request.md000064400000000000000000000011351046102023000217000ustar 00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- ## Problem A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] Include Issue links if they exist. Minimal Reproducible Examples (MRE) with few dependencies are useful. ## Solution A clear and concise description of what you want to happen. ## Alternatives A clear and concise description of any alternative solutions or features you've considered. ## Additional context Add any other context or screenshots about the feature request here. email_address-0.2.9/.github/ISSUE_TEMPLATE/spec_bug_report.md000064400000000000000000000014401046102023000216560ustar 00000000000000--- name: Specification Non-conformance report about: Report an error in our implementation title: '' labels: specification assignees: '' --- ## Describe the bug A clear and concise description of what the bug is. Please include references to the relevant specification; for example: > RFC 2616, section 4.3.2: > > > The HEAD method is identical to GET except that the server MUST NOT > > send a message body in the response ## Minimal Reproducible Examples (MRE) Please try to provide information which will help us to fix the issue faster. MREs with few dependencies are especially lovely <3. ## Additional context Add any other context about the problem here. For example, any other specifications that provide additional information, or other implementations that show common behavior. email_address-0.2.9/.github/dependabot.yml000064400000000000000000000002561046102023000166230ustar 00000000000000version: 2 updates: - package-ecosystem: cargo directory: "/" schedule: interval: daily time: "13:00" open-pull-requests-limit: 10 reviewers: - johnstonskj email_address-0.2.9/.github/workflows/rust.yml000064400000000000000000000145211046102023000175500ustar 00000000000000name: Rust on: pull_request: paths: - '**' - '!/*.md' - '!/*.org' - "!/LICENSE" push: branches: - main paths: - '**' - '!/*.md' - '!/*.org' - "!/LICENSE" schedule: - cron: '12 12 12 * *' jobs: publish: name: Publish (dry-run) needs: [test, docs] strategy: matrix: package: - email_address continue-on-error: true runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true - uses: Swatinem/rust-cache@v2 - name: Check publish uses: actions-rs/cargo@v1 with: command: publish args: --package ${{ matrix.package}} --dry-run check_tests: name: Check for test types runs-on: ubuntu-latest outputs: has_benchmarks: ${{ steps.check_benchmarks.outputs.has_benchmarks }} has_examples: ${{ steps.check_examples.outputs.has_examples }} steps: - name: Check for benchmarks id: check_benchmarks run: test -d benchmarks && echo "has_benchmarks=1" || echo "has_benchmarks=" >> $GITHUB_OUTPUT shell: bash - name: Check for examples id: check_examples run: test -d examples && echo "has_examples=1" || echo "has_examples=" >> $GITHUB_OUTPUT shell: bash test: name: Test needs: [rustfmt, clippy] strategy: matrix: os: ["ubuntu-latest", "windows-latest", "macos-latest"] rust: ["stable", "beta", "nightly"] test-features: ["", "--all-features", "--no-default-features"] continue-on-error: ${{ matrix.rust != 'stable' }} runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust }} profile: minimal override: true - uses: Swatinem/rust-cache@v2 - name: Build uses: actions-rs/cargo@v1 with: command: build args: --workspace ${{ matrix.test-features }} - name: Test uses: actions-rs/cargo@v1 with: command: test args: --workspace ${{ matrix.test-features }} benchmarks: name: Benchmarks needs: [rustfmt, clippy, check_tests] if: needs.check_tests.outputs.has_benchmarks strategy: matrix: os: ["ubuntu-latest", "windows-latest", "macos-latest"] rust: ["stable"] runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust }} profile: minimal override: true - uses: Swatinem/rust-cache@v2 - name: Run benchmarks with all features uses: actions-rs/cargo@v1 with: command: test args: --workspace --benches --all-features --no-fail-fast examples: name: Examples needs: [rustfmt, clippy, check_tests] if: needs.check_tests.outputs.has_examples runs-on: ubuntu-latest strategy: matrix: os: ["ubuntu-latest", "windows-latest", "macos-latest"] rust: ["stable"] steps: - name: Checkout repository uses: actions/checkout@v2 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust }} profile: minimal override: true - uses: Swatinem/rust-cache@v2 - name: Run examples with all features uses: actions-rs/cargo@v1 with: command: test args: --workspace --examples --all-features --no-fail-fast coverage: name: Code Coverage needs: test runs-on: ubuntu-latest strategy: matrix: os: ["ubuntu-latest", "windows-latest", "macos-latest"] rust: ["stable"] steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust }} override: true - name: Run cargo-tarpaulin uses: actions-rs/tarpaulin@v0.1 with: version: 0.22.0 args: --all-features -- --test-threads 1 - name: Upload to codecov.io uses: codecov/codecov-action@v1.0.2 with: token: ${{secrets.CODECOV_TOKEN}} - name: Archive code coverage results uses: actions/upload-artifact@v1 with: name: code-coverage-report path: cobertura.xml docs: name: Document generation needs: [rustfmt, clippy] runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true - uses: Swatinem/rust-cache@v2 - name: Generate documentation uses: actions-rs/cargo@v1 env: RUSTDOCFLAGS: -D warnings with: command: doc args: --workspace --all-features --no-deps rustfmt: name: rustfmt runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true components: rustfmt - uses: Swatinem/rust-cache@v2 - name: Check formatting uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check clippy: name: clippy runs-on: ubuntu-latest permissions: checks: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true components: clippy - uses: Swatinem/rust-cache@v2 - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} args: --workspace --no-deps --all-features --all-targets -- -D warnings email_address-0.2.9/.github/workflows/security-audit.yml000064400000000000000000000011741046102023000215260ustar 00000000000000name: Security audit on: push: paths: - '**/Cargo.toml' - '**/Cargo.lock' pull_request: paths: - '**/Cargo.toml' - '**/Cargo.lock' schedule: - cron: '12 12 12 * *' jobs: security_audit: runs-on: ubuntu-latest permissions: checks: write steps: - name: Checkout repository uses: actions/checkout@v3 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} email_address-0.2.9/.github/workflows/typos.yml000064400000000000000000000004101046102023000177210ustar 00000000000000name: Spelling on: [pull_request] jobs: spelling: name: Spell Check with Typos runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 - name: Spell check repository uses: crate-ci/typos@master email_address-0.2.9/.gitignore000064400000000000000000000016021046102023000144170ustar 00000000000000# -*- mode: gitignore; -*- ### Emacs ### *~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* # Org-mode .org-id-locations *_archive # flymake-mode *_flymake.* # eshell files /eshell/history /eshell/lastdir # elpa packages /elpa/ # reftex files *.rel # AUCTeX auto folder /auto/ # cask packages .cask/ dist/ # Flycheck flycheck_*.el # server auth directory /server/ # projectiles files .projectile # directory configuration .dir-locals.el # network security /network-security.data ### Rust ### # 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 # Generated by `cargo mutants` /mutants.out/ # Generated by `unused-features analyze` /report.json email_address-0.2.9/CODE_OF_CONDUCT.md000064400000000000000000000121471046102023000152340ustar 00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at johnstonskj@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. email_address-0.2.9/CONTRIBUTING.md000064400000000000000000000060161046102023000146640ustar 00000000000000# How to contribute I'm really glad you're reading this, because we need volunteer developers to help this project continue to grow and improve. 1. file [bugs](../../issues/new?assignees=&labels=bug&template=bug_report.md) and [enhancement requests](../../issues/new?assignees=&labels=enhancement&template=feature_request.md) 2. review the project documentation know if you find are issues, or missing content, there 3. Fix or Add something and send us a pull request; you may like to pick up one of the issues marked [help wanted](../../labels/help%20wanted) or [good first issue](../../labels/good%20first%20issue) as an introduction. Alternatively, [documentation](../../labels/documentation) issues can be a great way to understand the project and help improve the developer experience. ## Submitting changes We love pull requests from everyone. By participating in this project, you agree to abide by our [code of conduct](./CODE_OF_CONDUCT.md), and [License](./LICENSE). Fork, then clone the repo: ``` git clone git@github.com:johnstonskj/{{repository-name}}.git ``` Ensure you have a good Rust install, usually managed by [Rustup](https://rustup.rs/). You can ensure the latest tools with the following: ``` rustup update ``` Make sure the tests pass: ``` cargo test --package {{package-name}} --no-fail-fast -- --exact cargo test --package {{package-name}} --no-fail-fast --all-features -- --exact cargo test --package {{package-name}} --no-fail-fast --no-default-features -- --exact ``` Make your change. Add tests, and documentation, for your change. For tests please add a comment of the form: ```rust #[test] // Regression test: GitHub issue #11 // or // Feature test: GitHub PR: #15 fn test_something() { } ``` Ensure not only that tests pass, but the following all run successfully. ``` cargo doc --all-features --no-deps cargo fmt cargo clippy ``` If you have made any changes to `Cargo.toml`, also check: ``` cargo outdated --depth 1 cargo audit ``` Push to your fork and [submit a pull request](../../compare/) using our [template](./pull_request_template.md). At this point you're waiting on us. We like to at least comment on pull requests within three business days (and, typically, one business day). We may suggest some changes or improvements or alternatives. Some things that will increase the chance that your pull request is accepted: * Write unit tests. * Write API documentation. * Write a [good commit message](https://cbea.ms/git-commit/https://cbea.ms/git-commit/). ## Coding conventions The primary tool for coding conventions is rustfmt, and specifically `cargo fmt` is a part of the build process and will cause Actions to fail. DO NOT create or update any existing `rustfmt.toml` file to change the default formatting rules. DO NOT alter any `warn` or `deny` library attributes. DO NOT add any `feature` attributes that would prohibit building on the stable channel. In some cases new crate-level features can be used to introduce an unstable feature dependency but these MUST be clearly documented and optional. email_address-0.2.9/Cargo.toml0000644000000021670000000000100116440ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "email_address" version = "0.2.9" authors = ["Simon Johnston "] publish = true description = "A Rust crate providing an implementation of an RFC-compliant `EmailAddress` newtype. " documentation = "https://docs.rs/email_address/" readme = "README.md" license = "MIT" repository = "https://github.com/johnstonskj/rust-email_address.git" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] [dependencies.serde] version = "1.0" optional = true [dev-dependencies.claims] version = "0.7.1" [dev-dependencies.serde_assert] version = "0.8.0" [features] default = ["serde_support"] serde_support = ["serde"] email_address-0.2.9/Cargo.toml.orig000064400000000000000000000012751046102023000153240ustar 00000000000000[package] name = "email_address" version = "0.2.9" authors = ["Simon Johnston "] description = "A Rust crate providing an implementation of an RFC-compliant `EmailAddress` newtype. " documentation = "https://docs.rs/email_address/" repository = "https://github.com/johnstonskj/rust-email_address.git" edition = "2018" license = "MIT" readme = "README.md" publish = true [package.metadata.docs.rs] # This only builds a single target for documentation. targets = ["x86_64-unknown-linux-gnu"] [features] default = ["serde_support"] serde_support = ["serde"] [dependencies] serde = { optional = true, version = "1.0" } [dev-dependencies] claims = "0.7.1" serde_assert = "0.8.0" email_address-0.2.9/LICENSE000064400000000000000000000020571046102023000134410ustar 00000000000000MIT License Copyright (c) 2019 Simon Johnston Permission 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. email_address-0.2.9/README.md000064400000000000000000000123511046102023000137110ustar 00000000000000# Crate email_address A Rust crate providing an implementation of an RFC-compliant `EmailAddress` newtype. ![MIT License](https://img.shields.io/badge/license-mit-118811.svg) ![Minimum Rust Version](https://img.shields.io/badge/Min%20Rust-1.40-green.svg) [![crates.io](https://img.shields.io/crates/v/email_address.svg)](https://crates.io/crates/email_address) [![docs.rs](https://docs.rs/email_address/badge.svg)](https://docs.rs/email_address) ![Build](https://github.com/johnstonskj/rust-email_address/workflows/Rust/badge.svg) ![Audit](https://github.com/johnstonskj/rust-email_address/workflows/Security%20audit/badge.svg) [![GitHub stars](https://img.shields.io/github/stars/johnstonskj/rust-email_address.svg)](https://github.com/johnstonskj/rust-email_address/stargazers) Primarily for validation, the `EmailAddress` type is constructed with `FromStr::from_str` which will raise any parsing errors. Prior to constructions the functions `is_valid`, `is_valid_local_part`, and `is_valid_domain` may also be used to test for validity without constructing an instance. ## Status Currently, it supports all the RFC ASCII and UTF-8 character set rules as well as quoted and unquoted local parts but does not yet support all the productions required for SMTP headers; folding whitespace, comments, etc. ## Example ```rust use email_address::*; assert!(EmailAddress::is_valid("user.name+tag+sorting@example.com")); assert_eq!( EmailAddress::from_str("Abc.example.com"), Error::MissingSeparator.into() ); ``` ## Specifications 1. RFC 1123: [_Requirements for Internet Hosts -- Application and Support_](https://tools.ietf.org/html/rfc1123), IETF,Oct 1989. 1. RFC 3629: [_UTF-8, a transformation format of ISO 10646_](https://tools.ietf.org/html/rfc3629), IETF, Nov 2003. 1. RFC 3696: [_Application Techniques for Checking and Transformation of Names_](https://tools.ietf.org/html/rfc3696), IETF, Feb 2004. 1. RFC 4291 [_IP Version 6 Addressing Architecture_](https://tools.ietf.org/html/rfc4291), IETF, Feb 2006. 1. RFC 5234: [_Augmented BNF for Syntax Specifications: ABNF_](https://tools.ietf.org/html/rfc5234), IETF, Jan 2008. 1. RFC 5321: [_Simple Mail Transfer Protocol_](https://tools.ietf.org/html/rfc5321), IETF, Oct 2008. 1. RFC 5322: [_Internet Message Format_](https://tools.ietf.org/html/rfc5322), I ETF, Oct 2008. 1. RFC 5890: [_Internationalized Domain Names for Applications (IDNA): Definitions and Document Framework_](https://tools.ietf.org/html/rfc5890), IETF, Aug 2010. 1. RFC 6531: [_SMTP Extension for Internationalized Email_](https://tools.ietf.org/html/rfc6531), IETF, Feb 2012 1. RFC 6532: [_Internationalized Email Headers_](https://tools.ietf.org/html/rfc6532), IETF, Feb 2012. ## Changes ### Version 0.2.9 * Fixed bug [#21](https://github.com/johnstonskj/rust-email_address/issues/21): Invalid Unicode characters accepted. ### Version 0.2.8 * Fixed bug [#29](https://github.com/johnstonskj/rust-email_address/issues/29): Put back implementation of `Eq`. ### Version 0.2.7 * Feature: added builder functions to the `Option` type. * Documentation: added examples to the `Option` type documentation. ### Version 0.2.6 * Fix: merge issues. ### Version 0.2.5 * Feature: Pull Request #15 -- Potential enhancement to add any free-text as display name. * Feature: Pull Request #17 -- Check for non-alphanumeric character starting or ending domain parts. * Feature: Pull Request #18 -- Error with `SubDomainEmpty` when empty instead of `InvalidCharacter`. * Feature: Pull Request #19 -- Allow configuring minimum number of subdomains. * Feature: Pull Request #20 -- Add option to disallow domain literals. * Feature: Pull Request #22 -- Handle a single qoute in local part of email Thanks to [ghandic](https://github.com/ghandic), [blaine-arcjet](https://github.com/blaine-arcjet), [Thomasdezeeuw](https://github.com/Thomasdezeeuw). ### Version 0.2.4 * Fixed bug [#11](https://github.com/johnstonskj/rust-email_address/issues/11): 1. Add manual implementation of `PartialEq` with case insensitive comparison for domain part. 2. Add manual implementation of `Hash`, because above. * Change signature for `new_unchecked` to be more flexible. * Add `as_str` helper method. ### Version 0.2.3 * Added new `EmailAddress::new_unchecked` function ([Sören Meier](https://github.com/soerenmeier)). ### Version 0.2.2 * Removed manual `Send` and `Sync` implementation, and fixed documentation bug ([Sören Meier](https://github.com/soerenmeier)). ### Version 0.2.1 * Added `From` for `String`. * Added `AsRef" ^------------------^ email() ^-------^ domain() ^--------^ local_part() ^------------^ display_part() ``` # Example The following shoes the basic `is_valid` and `from_str` functions. ```rust use email_address::*; use std::str::FromStr; assert!(EmailAddress::is_valid("user.name+tag+sorting@example.com")); assert_eq!( EmailAddress::from_str("Abc.example.com"), Error::MissingSeparator.into() ); ``` The following shows the three format functions used to output an email address. ```rust use email_address::*; use std::str::FromStr; let email = EmailAddress::from_str("johnstonsk@gmail.com").unwrap(); assert_eq!( email.to_string(), "johnstonsk@gmail.com".to_string() ); assert_eq!( String::from(email.clone()), "johnstonsk@gmail.com".to_string() ); assert_eq!( email.as_ref(), "johnstonsk@gmail.com" ); assert_eq!( email.to_uri(), "mailto:johnstonsk@gmail.com".to_string() ); assert_eq!( email.to_display("Simon Johnston"), "Simon Johnston ".to_string() ); ``` # Specifications 1. RFC 1123: [_Requirements for Internet Hosts -- Application and Support_](https://tools.ietf.org/html/rfc1123), IETF,Oct 1989. 1. RFC 3629: [_UTF-8, a transformation format of ISO 10646_](https://tools.ietf.org/html/rfc3629), IETF, Nov 2003. 1. RFC 3696: [_Application Techniques for Checking and Transformation of Names_](https://tools.ietf.org/html/rfc3696), IETF, Feb 2004. 1. RFC 4291 [_IP Version 6 Addressing Architecture_](https://tools.ietf.org/html/rfc4291), IETF, Feb 2006. 1. RFC 5234: [_Augmented BNF for Syntax Specifications: ABNF_](https://tools.ietf.org/html/rfc5234), IETF, Jan 2008. 1. RFC 5321: [_Simple Mail Transfer Protocol_](https://tools.ietf.org/html/rfc5321), IETF, Oct 2008. 1. RFC 5322: [_Internet Message Format_](https://tools.ietf.org/html/rfc5322), I ETF, Oct 2008. 1. RFC 5890: [_Internationalized Domain Names for Applications (IDNA): Definitions and Document Framework_](https://tools.ietf.org/html/rfc5890), IETF, Aug 2010. 1. RFC 6531: [_SMTP Extension for Internationalized Email_](https://tools.ietf.org/html/rfc6531), IETF, Feb 2012 1. RFC 6532: [_Internationalized Email Headers_](https://tools.ietf.org/html/rfc6532), IETF, Feb 2012. From RFC 5322: §3.2.1. [Quoted characters](https://tools.ietf.org/html/rfc5322#section-3.2.1): ```ebnf quoted-pair = ("\" (VCHAR / WSP)) / obs-qp ``` From RFC 5322: §3.2.2. [Folding White Space and Comments](https://tools.ietf.org/html/rfc5322#section-3.2.2): ```ebnf FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space ctext = %d33-39 / ; Printable US-ASCII %d42-91 / ; characters not including %d93-126 / ; "(", ")", or "\" obs-ctext ccontent = ctext / quoted-pair / comment comment = "(" *([FWS] ccontent) [FWS] ")" CFWS = (1*([FWS] comment) [FWS]) / FWS ``` From RFC 5322: §3.2.3. [Atom](https://tools.ietf.org/html/rfc5322#section-3.2.3): ```ebnf atext = ALPHA / DIGIT / ; Printable US-ASCII "!" / "#" / ; characters not including "$" / "%" / ; specials. Used for atoms. "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" atom = [CFWS] 1*atext [CFWS] dot-atom-text = 1*atext *("." 1*atext) dot-atom = [CFWS] dot-atom-text [CFWS] specials = "(" / ")" / ; Special characters that do "<" / ">" / ; not appear in atext "[" / "]" / ":" / ";" / "@" / "\" / "," / "." / DQUOTE ``` From RFC 5322: §3.2.4. [Quoted Strings](https://tools.ietf.org/html/rfc5322#section-3.2.4): ```ebnf qtext = %d33 / ; Printable US-ASCII %d35-91 / ; characters not including %d93-126 / ; "\" or the quote character obs-qtext qcontent = qtext / quoted-pair quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS] ``` From RFC 5322, §3.4.1. [Addr-Spec Specification](https://tools.ietf.org/html/rfc5322#section-3.4.1): ```ebnf addr-spec = local-part "@" domain local-part = dot-atom / quoted-string / obs-local-part domain = dot-atom / domain-literal / obs-domain domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS] dtext = %d33-90 / ; Printable US-ASCII %d94-126 / ; characters not including obs-dtext ; "[", "]", or "\" ``` RFC 3696, §3. [Restrictions on email addresses](https://tools.ietf.org/html/rfc3696#section-3) describes in detail the quoting of characters in an address. ## Unicode RFC 6531, §3.3. [Extended Mailbox Address Syntax](https://tools.ietf.org/html/rfc6531#section-3.3) extends the rules above for non-ASCII character sets. ```ebnf sub-domain =/ U-label ; extend the definition of sub-domain in RFC 5321, Section 4.1.2 atext =/ UTF8-non-ascii ; extend the implicit definition of atext in ; RFC 5321, Section 4.1.2, which ultimately points to ; the actual definition in RFC 5322, Section 3.2.3 qtextSMTP =/ UTF8-non-ascii ; extend the definition of qtextSMTP in RFC 5321, Section 4.1.2 esmtp-value =/ UTF8-non-ascii ; extend the definition of esmtp-value in RFC 5321, Section 4.1.2 ``` A "U-label" is an IDNA-valid string of Unicode characters, in Normalization Form C (NFC) and including at least one non-ASCII character, expressed in a standard Unicode Encoding Form (such as UTF-8). It is also subject to the constraints about permitted characters that are specified in Section 4.2 of the Protocol document and the rules in the Sections 2 and 3 of the Tables document, the Bidi constraints in that document if it contains any character from scripts that are written right to left, and the symmetry constraint described immediately below. Conversions between U-labels and A-labels are performed according to the "Punycode" specification RFC3492, adding or removing the ACE prefix as needed. RFC 6532: §3.1 [UTF-8 Syntax and Normalization](https://tools.ietf.org/html/rfc6532#section-3.1), and §3.2 [Syntax Extensions to RFC 5322](https://tools.ietf.org/html/rfc6532#section-3.2) extend the syntax above with: ```ebnf UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4 ... VCHAR =/ UTF8-non-ascii ctext =/ UTF8-non-ascii atext =/ UTF8-non-ascii qtext =/ UTF8-non-ascii text =/ UTF8-non-ascii ; note that this upgrades the body to UTF-8 dtext =/ UTF8-non-ascii ``` These in turn refer to RFC 6529 §4. [Syntax of UTF-8 Byte Sequences](https://tools.ietf.org/html/rfc3629#section-4): > A UTF-8 string is a sequence of octets representing a sequence of UCS > characters. An octet sequence is valid UTF-8 only if it matches the > following syntax, which is derived from the rules for encoding UTF-8 > and is expressed in the ABNF of \[RFC2234\]. ```ebnf UTF8-octets = *( UTF8-char ) UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4 UTF8-1 = %x00-7F UTF8-2 = %xC2-DF UTF8-tail UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) / %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail ) UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) / %xF4 %x80-8F 2( UTF8-tail ) UTF8-tail = %x80-BF ``` Comments in addresses are discussed in RFC 5322 Appendix A.5. [White Space, Comments, and Other Oddities](https://tools.ietf.org/html/rfc5322#appendix-A.5). An informal description can be found on [Wikipedia](https://en.wikipedia.org/wiki/Email_address). */ #![warn( unknown_lints, // ---------- Stylistic absolute_paths_not_starting_with_crate, elided_lifetimes_in_paths, explicit_outlives_requirements, macro_use_extern_crate, nonstandard_style, /* group */ noop_method_call, rust_2018_idioms, single_use_lifetimes, trivial_casts, trivial_numeric_casts, // ---------- Future future_incompatible, /* group */ rust_2021_compatibility, /* group */ // ---------- Public missing_debug_implementations, missing_docs, unreachable_pub, // ---------- Unsafe unsafe_code, unsafe_op_in_unsafe_fn, // ---------- Unused unused, /* group */ )] #![deny( // ---------- Public exported_private_dependencies, // ---------- Deprecated anonymous_parameters, bare_trait_objects, ellipsis_inclusive_range_patterns, // ---------- Unsafe deref_nullptr, drop_bounds, dyn_drop, )] #[cfg(feature = "serde_support")] use serde::{Deserialize, Serialize, Serializer}; use std::fmt::{Debug, Display, Formatter}; use std::hash::Hash; use std::str::FromStr; // ------------------------------------------------------------------------------------------------ // Public Types // ------------------------------------------------------------------------------------------------ /// /// Error type used when parsing an address. /// #[derive(Debug, Clone, PartialEq)] pub enum Error { /// An invalid character was found in some component of the address. InvalidCharacter, /// The separator character between `local-part` and `domain` (character: '@') was missing. MissingSeparator, /// The `local-part` is an empty string. LocalPartEmpty, /// The `local-part` is is too long. LocalPartTooLong, /// The `domain` is an empty string. DomainEmpty, /// The `domain` is is too long. DomainTooLong, /// The `sub-domain` within the `domain` is empty. SubDomainEmpty, /// A `sub-domain` within the `domain` is is too long. SubDomainTooLong, /// Too few `sub-domain`s in `domain`. DomainTooFew, /// Invalid placement of the domain separator (character: '.'). DomainInvalidSeparator, /// The quotes (character: '"') around `local-part` are unbalanced. UnbalancedQuotes, /// A Comment within the either the `local-part`, or `domain`, was malformed. InvalidComment, /// An IP address in a `domain-literal` was malformed. InvalidIPAddress, /// A `domain-literal` was supplied, but is unsupported by parser configuration. UnsupportedDomainLiteral, /// Display name was supplied, but is unsupported by parser configuration. UnsupportedDisplayName, /// Display name was not supplied, but email starts with '<'. MissingDisplayName, /// An email enclosed within <...> is missing the final '>'. MissingEndBracket, } /// /// Struct of options that can be configured when parsing with `parse_with_options`. /// #[derive(Debug, Copy, Clone)] pub struct Options { /// /// Sets the minimum number of domain segments that must exist to parse successfully. /// /// ```rust /// use email_address::*; /// /// assert!( /// EmailAddress::parse_with_options( /// "simon@localhost", /// Options::default().with_no_minimum_sub_domains(), /// ).is_ok() /// ); /// assert_eq!( /// EmailAddress::parse_with_options( /// "simon@localhost", /// Options::default().with_required_tld() /// ), /// Err(Error::DomainTooFew) /// ); /// ``` /// pub minimum_sub_domains: usize, /// /// Specifies if domain literals are allowed. Defaults to `true`. /// /// ```rust /// use email_address::*; /// /// assert!( /// EmailAddress::parse_with_options( /// "email@[127.0.0.256]", /// Options::default().with_domain_literal() /// ).is_ok() /// ); /// /// assert_eq!( /// EmailAddress::parse_with_options( /// "email@[127.0.0.256]", /// Options::default().without_domain_literal() /// ), /// Err(Error::UnsupportedDomainLiteral), /// ); /// ``` /// pub allow_domain_literal: bool, /// /// Specified whether display text is allowed. Defaults to `true`. If you want strict /// email-only checking setting this to `false` will remove support for the prefix string /// and therefore the '<' and '>' brackets around the email part. /// /// ```rust /// use email_address::*; /// /// assert_eq!( /// EmailAddress::parse_with_options( /// "Simon ", /// Options::default().without_display_text() /// ), /// Err(Error::UnsupportedDisplayName), /// ); /// /// assert_eq!( /// EmailAddress::parse_with_options( /// "", /// Options::default().without_display_text() /// ), /// Err(Error::InvalidCharacter), /// ); /// ``` /// pub allow_display_text: bool, } /// /// Type representing a single email address. This is basically a wrapper around a String, the /// email address is parsed for correctness with `FromStr::from_str`, which is the only want to /// create an instance. The various components of the email _are not_ parsed out to be accessible /// independently. /// #[derive(Debug, Clone)] pub struct EmailAddress(String); // ------------------------------------------------------------------------------------------------ // Implementations // ------------------------------------------------------------------------------------------------ const LOCAL_PART_MAX_LENGTH: usize = 64; // see: https://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690 const DOMAIN_MAX_LENGTH: usize = 254; const SUB_DOMAIN_MAX_LENGTH: usize = 63; #[allow(dead_code)] const CR: char = '\r'; #[allow(dead_code)] const LF: char = '\n'; const SP: char = ' '; const HTAB: char = '\t'; const ESC: char = '\\'; const AT: char = '@'; const DOT: char = '.'; const DQUOTE: char = '"'; const LBRACKET: char = '['; const RBRACKET: char = ']'; #[allow(dead_code)] const LPAREN: char = '('; #[allow(dead_code)] const RPAREN: char = ')'; const DISPLAY_SEP: &str = " <"; const DISPLAY_START: char = '<'; const DISPLAY_END: char = '>'; const MAILTO_URI_PREFIX: &str = "mailto:"; // ------------------------------------------------------------------------------------------------ impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Error::InvalidCharacter => write!(f, "Invalid character."), Error::LocalPartEmpty => write!(f, "Local part is empty."), Error::LocalPartTooLong => write!( f, "Local part is too long. Length limit: {}", LOCAL_PART_MAX_LENGTH ), Error::DomainEmpty => write!(f, "Domain is empty."), Error::DomainTooLong => { write!(f, "Domain is too long. Length limit: {}", DOMAIN_MAX_LENGTH) } Error::SubDomainEmpty => write!(f, "A sub-domain is empty."), Error::SubDomainTooLong => write!( f, "A sub-domain is too long. Length limit: {}", SUB_DOMAIN_MAX_LENGTH ), Error::MissingSeparator => write!(f, "Missing separator character '{}'.", AT), Error::DomainTooFew => write!(f, "Too few parts in the domain"), Error::DomainInvalidSeparator => { write!(f, "Invalid placement of the domain separator '{:?}", DOT) } Error::InvalidIPAddress => write!(f, "Invalid IP Address specified for domain."), Error::UnbalancedQuotes => write!(f, "Quotes around the local-part are unbalanced."), Error::InvalidComment => write!(f, "A comment was badly formed."), Error::UnsupportedDomainLiteral => write!(f, "Domain literals are not supported."), Error::UnsupportedDisplayName => write!(f, "Display names are not supported."), Error::MissingDisplayName => write!( f, "Display name was not supplied, but email starts with '<'." ), Error::MissingEndBracket => write!(f, "Terminating '>' is missing."), } } } impl std::error::Error for Error {} impl From for std::result::Result { fn from(err: Error) -> Self { Err(err) } } // ------------------------------------------------------------------------------------------------ impl Default for Options { fn default() -> Self { Self { minimum_sub_domains: Default::default(), allow_domain_literal: true, allow_display_text: true, } } } impl Options { /// Set the value of `minimum_sub_domains`. #[inline(always)] pub const fn with_minimum_sub_domains(self, min: usize) -> Self { Self { minimum_sub_domains: min, ..self } } #[inline(always)] /// Set the value of `minimum_sub_domains` to zero. pub const fn with_no_minimum_sub_domains(self) -> Self { Self { minimum_sub_domains: 0, ..self } } #[inline(always)] /// Set the value of `minimum_sub_domains` to two, this has the effect of requiring a /// domain name with a top-level domain (TLD). pub const fn with_required_tld(self) -> Self { Self { minimum_sub_domains: 2, ..self } } /// Set the value of `allow_domain_literal` to `true`. #[inline(always)] pub const fn with_domain_literal(self) -> Self { Self { allow_domain_literal: true, ..self } } /// Set the value of `allow_domain_literal` to `false`. #[inline(always)] pub const fn without_domain_literal(self) -> Self { Self { allow_domain_literal: false, ..self } } /// Set the value of `allow_display_text` to `true`. #[inline(always)] pub const fn with_display_text(self) -> Self { Self { allow_display_text: true, ..self } } /// Set the value of `allow_display_text` to `false`. #[inline(always)] pub const fn without_display_text(self) -> Self { Self { allow_display_text: false, ..self } } } // ------------------------------------------------------------------------------------------------ impl Display for EmailAddress { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } // From RFC 5321, section 2.4: // // The local-part of a mailbox MUST BE treated as case sensitive. Therefore, // SMTP implementations MUST take care to preserve the case of mailbox // local-parts. In particular, for some hosts, the user "smith" is different // from the user "Smith". However, exploiting the case sensitivity of mailbox // local-parts impedes interoperability and is discouraged. Mailbox domains // follow normal DNS rules and are hence not case sensitive. // impl PartialEq for EmailAddress { fn eq(&self, other: &Self) -> bool { let (left, right) = split_at(&self.0).unwrap(); let (other_left, other_right) = split_at(&other.0).unwrap(); left.eq(other_left) && right.eq_ignore_ascii_case(other_right) } } impl Eq for EmailAddress {} impl Hash for EmailAddress { fn hash(&self, state: &mut H) { self.0.hash(state); } } impl FromStr for EmailAddress { type Err = Error; fn from_str(s: &str) -> Result { parse_address(s, Default::default()) } } impl From for String { fn from(email: EmailAddress) -> Self { email.0 } } impl AsRef for EmailAddress { fn as_ref(&self) -> &str { &self.0 } } #[cfg(feature = "serde_support")] impl Serialize for EmailAddress { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.0) } } #[cfg(feature = "serde_support")] impl<'de> Deserialize<'de> for EmailAddress { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { use serde::de::{Error, Unexpected, Visitor}; struct EmailAddressVisitor; impl Visitor<'_> for EmailAddressVisitor { type Value = EmailAddress; fn expecting(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { fmt.write_str("string containing a valid email address") } fn visit_str(self, s: &str) -> Result where E: Error, { EmailAddress::from_str(s).map_err(|err| { let exp = format!("{}", err); Error::invalid_value(Unexpected::Str(s), &exp.as_ref()) }) } } deserializer.deserialize_str(EmailAddressVisitor) } } impl EmailAddress { /// /// Creates an `EmailAddress` without checking if the email is valid. Only /// call this method if the address is known to be valid. /// /// ``` /// use std::str::FromStr; /// use email_address::EmailAddress; /// /// let unchecked = "john.doe@example.com"; /// let email = EmailAddress::from_str(unchecked).expect("email is not valid"); /// let valid_email = String::from(email); /// let email = EmailAddress::new_unchecked(valid_email); /// /// assert_eq!("John Doe ", email.to_display("John Doe")); /// ``` pub fn new_unchecked(address: S) -> Self where S: Into, { Self(address.into()) } /// /// Parses an [EmailAddress] with custom [Options]. Useful for configuring validations /// that aren't mandatory by the specification. /// /// ``` /// use email_address::{EmailAddress, Options}; /// /// let options = Options { minimum_sub_domains: 2, ..Options::default() }; /// let result = EmailAddress::parse_with_options("john.doe@localhost", options).is_ok(); /// /// assert_eq!(result, false); /// ``` pub fn parse_with_options(address: &str, options: Options) -> Result { parse_address(address, options) } /// /// Determine whether the `address` string is a valid email address. Note this is equivalent to /// the following: /// /// ```rust /// use email_address::*; /// use std::str::FromStr; /// /// let is_valid = EmailAddress::from_str("johnstonskj@gmail.com").is_ok(); /// ``` /// pub fn is_valid(address: &str) -> bool { Self::from_str(address).is_ok() } /// /// Determine whether the `part` string would be a valid `local-part` if it were in an /// email address. /// pub fn is_valid_local_part(part: &str) -> bool { parse_local_part(part, Default::default()).is_ok() } /// /// Determine whether the `part` string would be a valid `domain` if it were in an /// email address. /// pub fn is_valid_domain(part: &str) -> bool { parse_domain(part, Default::default()).is_ok() } /// /// Return this email address formatted as a URI. This will also URI-encode the email /// address itself. So, `name@example.org` becomes `mailto:name@example.org`. /// /// ```rust /// use email_address::*; /// use std::str::FromStr; /// /// assert_eq!( /// EmailAddress::from_str("name@example.org").unwrap().to_uri(), /// String::from("mailto:name@example.org") /// ); /// ``` /// pub fn to_uri(&self) -> String { let encoded = encode(&self.0); format!("{}{}", MAILTO_URI_PREFIX, encoded) } /// /// Return a string formatted as a display email with the user name. This is commonly used /// in email headers and other locations where a display name is associated with the /// address. /// /// ```rust /// use email_address::*; /// use std::str::FromStr; /// /// assert_eq!( /// EmailAddress::from_str("name@example.org").unwrap().to_display("My Name"), /// String::from("My Name ") /// ); /// ``` /// pub fn to_display(&self, display_name: &str) -> String { format!("{} <{}>", display_name, self) } /// /// Returns the local part of the email address. This is borrowed so that no additional /// allocation is required. /// /// ```rust /// use email_address::*; /// use std::str::FromStr; /// /// assert_eq!( /// EmailAddress::from_str("name@example.org").unwrap().local_part(), /// String::from("name") /// ); /// ``` /// pub fn local_part(&self) -> &str { let (local, _, _) = split_parts(&self.0).unwrap(); local } /// /// Returns the display part of the email address. This is borrowed so that no additional /// allocation is required. /// /// ```rust /// use email_address::*; /// use std::str::FromStr; /// /// assert_eq!( /// EmailAddress::from_str("Name ").unwrap().display_part(), /// String::from("Name") /// ); /// ``` /// pub fn display_part(&self) -> &str { let (_, _, display) = split_parts(&self.0).unwrap(); display } /// /// Returns the email part of the email address. This is borrowed so that no additional /// allocation is required. /// /// ```rust /// use email_address::*; /// use std::str::FromStr; /// /// assert_eq!( /// EmailAddress::from_str("Name ").unwrap().email(), /// String::from("name@example.org") /// ); /// ``` /// pub fn email(&self) -> String { let (local, domain, _) = split_parts(&self.0).unwrap(); format!("{}{AT}{}", local, domain) } /// /// Returns the domain of the email address. This is borrowed so that no additional /// allocation is required. /// /// ```rust /// use email_address::*; /// use std::str::FromStr; /// /// assert_eq!( /// EmailAddress::from_str("name@example.org").unwrap().domain(), /// String::from("example.org") /// ); /// ``` /// pub fn domain(&self) -> &str { let (_, domain, _) = split_parts(&self.0).unwrap(); domain } /// /// Returns the entire email address as a string reference. /// pub fn as_str(&self) -> &str { self.as_ref() } } // ------------------------------------------------------------------------------------------------ // Private Functions // ------------------------------------------------------------------------------------------------ fn encode(address: &str) -> String { let mut result = String::new(); for c in address.chars() { if is_uri_reserved(c) { result.push_str(&format!("%{:02X}", c as u8)) } else { result.push(c); } } result } fn is_uri_reserved(c: char) -> bool { // No need to encode '@' as this is allowed in the email scheme. c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' || c == ',' || c == '/' || c == ':' || c == ';' || c == '=' || c == '?' || c == '[' || c == ']' } fn parse_address(address: &str, options: Options) -> Result { // // Deals with cases of '@' in `local-part`, if it is quoted they are legal, if // not then they'll return an `InvalidCharacter` error later. // let (local_part, domain, display) = split_parts(address)?; match ( display.is_empty(), local_part.starts_with(DISPLAY_START), options.allow_display_text, ) { (false, _, false) => Err(Error::UnsupportedDisplayName), (true, true, true) => Err(Error::MissingDisplayName), (true, true, false) => Err(Error::InvalidCharacter), _ => { parse_local_part(local_part, options)?; parse_domain(domain, options)?; Ok(EmailAddress(address.to_owned())) } } } fn split_parts(address: &str) -> Result<(&str, &str, &str), Error> { let (display, email) = split_display_email(address)?; let (local_part, domain) = split_at(email)?; Ok((local_part, domain, display)) } fn split_display_email(text: &str) -> Result<(&str, &str), Error> { match text.rsplit_once(DISPLAY_SEP) { None => Ok(("", text)), Some((left, right)) => { let right = right.trim(); if !right.ends_with(DISPLAY_END) { Err(Error::MissingEndBracket) } else { let email = &right[0..right.len() - 1]; let display_name = left.trim(); Ok((display_name, email)) } } } } fn split_at(address: &str) -> Result<(&str, &str), Error> { match address.rsplit_once(AT) { None => Error::MissingSeparator.into(), Some(left_right) => Ok(left_right), } } fn parse_local_part(part: &str, _: Options) -> Result<(), Error> { if part.is_empty() { Error::LocalPartEmpty.into() } else if part.len() > LOCAL_PART_MAX_LENGTH { Error::LocalPartTooLong.into() } else if part.starts_with(DQUOTE) && part.ends_with(DQUOTE) { // <= to handle `part` = `"` (single quote). if part.len() <= 2 { Error::LocalPartEmpty.into() } else { parse_quoted_local_part(&part[1..part.len() - 1]) } } else { parse_unquoted_local_part(part) } } fn parse_quoted_local_part(part: &str) -> Result<(), Error> { if is_qcontent(part) { Ok(()) } else { Error::InvalidCharacter.into() } } fn parse_unquoted_local_part(part: &str) -> Result<(), Error> { if is_dot_atom_text(part) { Ok(()) } else { Error::InvalidCharacter.into() } } fn parse_domain(part: &str, options: Options) -> Result<(), Error> { if part.is_empty() { Error::DomainEmpty.into() } else if part.len() > DOMAIN_MAX_LENGTH { Error::DomainTooLong.into() } else if part.starts_with(LBRACKET) && part.ends_with(RBRACKET) { if options.allow_domain_literal { parse_literal_domain(&part[1..part.len() - 1]) } else { Error::UnsupportedDomainLiteral.into() } } else { parse_text_domain(part, options) } } fn parse_text_domain(part: &str, options: Options) -> Result<(), Error> { let mut sub_domains = 0; for sub_part in part.split(DOT) { // As per https://www.rfc-editor.org/rfc/rfc1034#section-3.5 // and https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address, // at least one character must exist in a `subdomain`/`label` part of the domain if sub_part.is_empty() { return Error::SubDomainEmpty.into(); } // As per https://www.rfc-editor.org/rfc/rfc1034#section-3.5, // the domain label needs to start with a `letter`; // however, https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address // specifies a label can start // with a `let-dig` (letter or digit), so we allow the wider range if !sub_part.starts_with(char::is_alphanumeric) { return Error::InvalidCharacter.into(); } // Both specifications mentioned above require the last character to be a // `let-dig` (letter or digit) if !sub_part.ends_with(char::is_alphanumeric) { return Error::InvalidCharacter.into(); } if sub_part.len() > SUB_DOMAIN_MAX_LENGTH { return Error::SubDomainTooLong.into(); } if !is_atom(sub_part) { return Error::InvalidCharacter.into(); } sub_domains += 1; } if sub_domains < options.minimum_sub_domains { Error::DomainTooFew.into() } else { Ok(()) } } fn parse_literal_domain(part: &str) -> Result<(), Error> { if part.chars().all(is_dtext_char) { return Ok(()); } Error::InvalidCharacter.into() } // ------------------------------------------------------------------------------------------------ fn is_atext(c: char) -> bool { c.is_alphanumeric() || c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || c == '/' || c == '=' || c == '?' || c == '^' || c == '_' || c == '`' || c == '{' || c == '|' || c == '}' || c == '~' || is_utf8_non_ascii(c) } //fn is_special(c: char) -> bool { // c == '(' // || c == ')' // || c == '<' // || c == '>' // || c == '[' // || c == ']' // || c == ':' // || c == ';' // || c == '@' // || c == '\\' // || c == ',' // || c == '.' // || c == DQUOTE //} fn is_utf8_non_ascii(c: char) -> bool { let bytes = (c as u32).to_be_bytes(); // UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4 match (bytes[0], bytes[1], bytes[2], bytes[3]) { // UTF8-2 = %xC2-DF UTF8-tail (0x00, 0x00, 0xC2..=0xDF, 0x80..=0xBF) => true, // UTF8-3 = %xE0 %xA0-BF UTF8-tail / // %xE1-EC 2( UTF8-tail ) / // %xED %x80-9F UTF8-tail / // %xEE-EF 2( UTF8-tail ) (0x00, 0xE0, 0xA0..=0xBF, 0x80..=0xBF) => true, (0x00, 0xE1..=0xEC, 0x80..=0xBF, 0x80..=0xBF) => true, (0x00, 0xED, 0x80..=0x9F, 0x80..=0xBF) => true, (0x00, 0xEE..=0xEF, 0x80..=0xBF, 0x80..=0xBF) => true, // UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / // %xF1-F3 3( UTF8-tail ) / // %xF4 %x80-8F 2( UTF8-tail ) (0xF0, 0x90..=0xBF, 0x80..=0xBF, 0x80..=0xBF) => true, (0xF1..=0xF3, 0x80..=0xBF, 0x80..=0xBF, 0x80..=0xBF) => true, (0xF4, 0x80..=0x8F, 0x80..=0xBF, 0x80..=0xBF) => true, // UTF8-tail = %x80-BF _ => false, } } fn is_atom(s: &str) -> bool { !s.is_empty() && s.chars().all(is_atext) } fn is_dot_atom_text(s: &str) -> bool { s.split(DOT).all(is_atom) } fn is_vchar(c: char) -> bool { ('\x21'..='\x7E').contains(&c) } fn is_wsp(c: char) -> bool { c == SP || c == HTAB } fn is_qtext_char(c: char) -> bool { c == '\x21' || ('\x23'..='\x5B').contains(&c) || ('\x5D'..='\x7E').contains(&c) || is_utf8_non_ascii(c) } fn is_qcontent(s: &str) -> bool { let mut char_iter = s.chars(); while let Some(c) = &char_iter.next() { if c == &ESC { // quoted-pair match char_iter.next() { Some(c2) if is_vchar(c2) => (), _ => return false, } } else if !(is_wsp(*c) || is_qtext_char(*c)) { // qtext return false; } } true } fn is_dtext_char(c: char) -> bool { ('\x21'..='\x5A').contains(&c) || ('\x5E'..='\x7E').contains(&c) || is_utf8_non_ascii(c) } //fn is_ctext_char(c: char) -> bool { // (c >= '\x21' && c == '\x27') // || ('\x2A'..='\x5B').contains(&c) // || ('\x5D'..='\x7E').contains(&c) // || is_utf8_non_ascii(c) //} // //fn is_ctext(s: &str) -> bool { // s.chars().all(is_ctext_char) //} // ------------------------------------------------------------------------------------------------ // Unit Tests // ------------------------------------------------------------------------------------------------ #[cfg(feature = "serde_support")] #[cfg(test)] mod serde_tests { use super::*; use claims::{assert_err_eq, assert_ok, assert_ok_eq}; use serde::de::{Error as _, Unexpected}; use serde_assert::{Deserializer, Serializer, Token}; #[test] fn test_serialize() { let email = assert_ok!(EmailAddress::from_str("simple@example.com")); let serializer = Serializer::builder().build(); assert_ok_eq!( email.serialize(&serializer), [Token::Str("simple@example.com".to_owned())] ); } #[test] fn test_deserialize() { let mut deserializer = Deserializer::builder([Token::Str("simple@example.com".to_owned())]).build(); let email = assert_ok!(EmailAddress::from_str("simple@example.com")); assert_ok_eq!(EmailAddress::deserialize(&mut deserializer), email); } #[test] fn test_deserialize_invalid_value() { let mut deserializer = Deserializer::builder([Token::Str("Abc.example.com".to_owned())]).build(); assert_err_eq!( EmailAddress::deserialize(&mut deserializer), serde_assert::de::Error::invalid_value( Unexpected::Str("Abc.example.com"), &"Missing separator character '@'." ) ); } #[test] fn test_deserialize_invalid_type() { let mut deserializer = Deserializer::builder([Token::U64(42)]).build(); assert_err_eq!( EmailAddress::deserialize(&mut deserializer), serde_assert::de::Error::invalid_type( Unexpected::Unsigned(42), &"string containing a valid email address" ) ); } // Regression test: GitHub issue #26 #[test] fn test_serde_roundtrip() { let email = assert_ok!(EmailAddress::from_str("simple@example.com")); let serializer = Serializer::builder().build(); let mut deserializer = Deserializer::builder(assert_ok!(email.serialize(&serializer))).build(); assert_ok_eq!(EmailAddress::deserialize(&mut deserializer), email); } } #[cfg(test)] mod tests { use super::*; fn is_valid(address: &str, test_case: Option<&str>) { if let Some(test_case) = test_case { println!(">> test case: {}", test_case); println!(" <{}>", address); } else { println!(">> <{}>", address); } assert!(EmailAddress::is_valid(address)); } fn valid_with_options(address: &str, options: Options, test_case: Option<&str>) { if let Some(test_case) = test_case { println!(">> test case: {}", test_case); println!(" <{}>", address); } else { println!(">> <{}>", address); } assert!(EmailAddress::parse_with_options(address, options).is_ok()); } #[test] fn test_good_examples_from_wikipedia_01() { is_valid("simple@example.com", None); } #[test] fn test_good_examples_from_wikipedia_02() { is_valid("very.common@example.com", None); } #[test] fn test_good_examples_from_wikipedia_03() { is_valid("disposable.style.email.with+symbol@example.com", None); } #[test] fn test_good_examples_from_wikipedia_04() { is_valid("other.email-with-hyphen@example.com", None); } #[test] fn test_good_examples_from_wikipedia_05() { is_valid("fully-qualified-domain@example.com", None); } #[test] fn test_good_examples_from_wikipedia_06() { is_valid( "user.name+tag+sorting@example.com", Some(" may go to user.name@example.com inbox depending on mail server"), ); } #[test] fn test_good_examples_from_wikipedia_07() { is_valid("x@example.com", Some("one-letter local-part")); } #[test] fn test_good_examples_from_wikipedia_08() { is_valid("example-indeed@strange-example.com", None); } #[test] fn test_good_examples_from_wikipedia_09() { is_valid( "admin@mailserver1", Some("local domain name with no TLD, although ICANN highly discourages dotless email addresses") ); } #[test] fn test_good_examples_from_wikipedia_10() { is_valid( "example@s.example", Some("see the List of Internet top-level domains"), ); } #[test] fn test_good_examples_from_wikipedia_11() { is_valid("\" \"@example.org", Some("space between the quotes")); } #[test] fn test_good_examples_from_wikipedia_12() { is_valid("\"john..doe\"@example.org", Some("quoted double dot")); } #[test] fn test_good_examples_from_wikipedia_13() { is_valid( "mailhost!username@example.org", Some("bangified host route used for uucp mailers"), ); } #[test] fn test_good_examples_from_wikipedia_14() { is_valid( "user%example.com@example.org", Some("% escaped mail route to user@example.com via example.org"), ); } #[test] fn test_good_examples_from_wikipedia_15() { is_valid("jsmith@[192.168.2.1]", None); } #[test] fn test_good_examples_from_wikipedia_16() { is_valid("jsmith@[IPv6:2001:db8::1]", None); } #[test] fn test_good_examples_from_wikipedia_17() { is_valid("user+mailbox/department=shipping@example.com", None); } #[test] fn test_good_examples_from_wikipedia_18() { is_valid("!#$%&'*+-/=?^_`.{|}~@example.com", None); } #[test] fn test_good_examples_from_wikipedia_19() { // '@' is allowed in a quoted local part. Sorry. is_valid("\"Abc@def\"@example.com", None); } #[test] fn test_good_examples_from_wikipedia_20() { is_valid("\"Joe.\\\\Blow\"@example.com", None); } #[test] fn test_good_examples_from_wikipedia_21() { is_valid("用户@例子.广告", Some("Chinese")); } #[test] fn test_good_examples_from_wikipedia_22() { is_valid("अजय@डाटा.भारत", Some("Hindi")); } #[test] fn test_good_examples_from_wikipedia_23() { is_valid("квіточка@пошта.укр", Some("Ukranian")); } #[test] fn test_good_examples_from_wikipedia_24() { is_valid("θσερ@εχαμπλε.ψομ", Some("Greek")); } #[test] fn test_good_examples_from_wikipedia_25() { is_valid("Dörte@Sörensen.example.com", Some("German")); } #[test] fn test_good_examples_from_wikipedia_26() { is_valid("коля@пример.рф", Some("Russian")); } #[test] fn test_good_examples_01() { valid_with_options( "foo@example.com", Options { minimum_sub_domains: 2, ..Default::default() }, Some("minimum sub domains"), ); } #[test] fn test_good_examples_02() { valid_with_options( "email@[127.0.0.256]", Options { allow_domain_literal: true, ..Default::default() }, Some("minimum sub domains"), ); } #[test] fn test_good_examples_03() { valid_with_options( "email@[2001:db8::12345]", Options { allow_domain_literal: true, ..Default::default() }, Some("minimum sub domains"), ); } #[test] fn test_good_examples_04() { valid_with_options( "email@[2001:db8:0:0:0:0:1]", Options { allow_domain_literal: true, ..Default::default() }, Some("minimum sub domains"), ); } #[test] fn test_good_examples_05() { valid_with_options( "email@[::ffff:127.0.0.256]", Options { allow_domain_literal: true, ..Default::default() }, Some("minimum sub domains"), ); } #[test] fn test_good_examples_06() { valid_with_options( "email@[2001:dg8::1]", Options { allow_domain_literal: true, ..Default::default() }, Some("minimum sub domains"), ); } #[test] fn test_good_examples_07() { valid_with_options( "email@[2001:dG8:0:0:0:0:0:1]", Options { allow_domain_literal: true, ..Default::default() }, Some("minimum sub domains"), ); } #[test] fn test_good_examples_08() { valid_with_options( "email@[::fTzF:127.0.0.1]", Options { allow_domain_literal: true, ..Default::default() }, Some("minimum sub domains"), ); } // ------------------------------------------------------------------------------------------------ #[test] fn test_to_strings() { let email = EmailAddress::from_str("коля@пример.рф").unwrap(); assert_eq!(String::from(email.clone()), String::from("коля@пример.рф")); assert_eq!(email.to_string(), String::from("коля@пример.рф")); assert_eq!(email.as_ref(), "коля@пример.рф"); } #[test] fn test_to_display() { let email = EmailAddress::from_str("коля@пример.рф").unwrap(); assert_eq!( email.to_display("коля"), String::from("коля <коля@пример.рф>") ); } #[test] fn test_touri() { let email = EmailAddress::from_str("коля@пример.рф").unwrap(); assert_eq!(email.to_uri(), String::from("mailto:коля@пример.рф")); } // ------------------------------------------------------------------------------------------------ fn expect(address: &str, error: Error, test_case: Option<&str>) { if let Some(test_case) = test_case { println!(">> test case: {}", test_case); println!(" <{}>, expecting {:?}", address, error); } else { println!(">> <{}>, expecting {:?}", address, error); } assert_eq!(EmailAddress::from_str(address), error.into()); } fn expect_with_options(address: &str, options: Options, error: Error, test_case: Option<&str>) { if let Some(test_case) = test_case { println!(">> test case: {}", test_case); println!(" <{}>, expecting {:?}", address, error); } else { println!(">> <{}>, expecting {:?}", address, error); } assert_eq!( EmailAddress::parse_with_options(address, options), error.into() ); } #[test] fn test_bad_examples_from_wikipedia_00() { expect( "Abc.example.com", Error::MissingSeparator, Some("no @ character"), ); } #[test] fn test_bad_examples_from_wikipedia_01() { expect( "A@b@c@example.com", Error::InvalidCharacter, Some("only one @ is allowed outside quotation marks"), ); } #[test] fn test_bad_examples_from_wikipedia_02() { expect( "a\"b(c)d,e:f;gi[j\\k]l@example.com", Error::InvalidCharacter, Some("none of the special characters in this local-part are allowed outside quotation marks") ); } #[test] fn test_bad_examples_from_wikipedia_03() { expect( "just\"not\"right@example.com", Error::InvalidCharacter, Some( "quoted strings must be dot separated or the only element making up the local-part", ), ); } #[test] fn test_bad_examples_from_wikipedia_04() { expect( "this is\"not\\allowed@example.com", Error::InvalidCharacter, Some("spaces, quotes, and backslashes may only exist when within quoted strings and preceded by a backslash") ); } #[test] fn test_bad_examples_from_wikipedia_05() { expect( "this\\ still\"not\\allowed@example.com", Error::InvalidCharacter, Some("even if escaped (preceded by a backslash), spaces, quotes, and backslashes must still be contained by quotes") ); } #[test] fn test_bad_examples_from_wikipedia_06() { expect( "1234567890123456789012345678901234567890123456789012345678901234+x@example.com", Error::LocalPartTooLong, Some("local part is longer than 64 characters"), ); } #[test] fn test_bad_example_01() { expect( "foo@example.v1234567890123456789012345678901234567890123456789012345678901234v.com", Error::SubDomainTooLong, Some("domain part is longer than 64 characters"), ); } #[test] fn test_bad_example_02() { expect( "@example.com", Error::LocalPartEmpty, Some("local-part is empty"), ); } #[test] fn test_bad_example_03() { expect( "\"\"@example.com", Error::LocalPartEmpty, Some("local-part is empty"), ); expect( "\"@example.com", Error::LocalPartEmpty, Some("local-part is empty"), ); } #[test] fn test_bad_example_04() { expect("simon@", Error::DomainEmpty, Some("domain is empty")); } #[test] fn test_bad_example_05() { expect( "example@invalid-.com", Error::InvalidCharacter, Some("domain label ends with hyphen"), ); } #[test] fn test_bad_example_06() { expect( "example@-invalid.com", Error::InvalidCharacter, Some("domain label starts with hyphen"), ); } #[test] fn test_bad_example_07() { expect( "example@invalid.com-", Error::InvalidCharacter, Some("domain label starts ends hyphen"), ); } #[test] fn test_bad_example_08() { expect( "example@inv-.alid-.com", Error::InvalidCharacter, Some("subdomain label ends hyphen"), ); } #[test] fn test_bad_example_09() { expect( "example@-inv.alid-.com", Error::InvalidCharacter, Some("subdomain label starts hyphen"), ); } #[test] fn test_bad_example_10() { expect( "example@-.com", Error::InvalidCharacter, Some("domain label is hyphen"), ); } #[test] fn test_bad_example_11() { expect( "example@-", Error::InvalidCharacter, Some("domain label is hyphen"), ); } #[test] fn test_bad_example_12() { expect( "example@-abc", Error::InvalidCharacter, Some("domain label starts with hyphen"), ); } #[test] fn test_bad_example_13() { expect( "example@abc-", Error::InvalidCharacter, Some("domain label ends with hyphen"), ); } #[test] fn test_bad_example_14() { expect( "example@.com", Error::SubDomainEmpty, Some("subdomain label is empty"), ); } #[test] fn test_bad_example_15() { expect_with_options( "foo@localhost", Options::default().with_minimum_sub_domains(2), Error::DomainTooFew, Some("too few domains"), ); } #[test] fn test_bad_example_16() { expect_with_options( "foo@a.b.c.d.e.f.g.h.i", Options::default().with_minimum_sub_domains(10), Error::DomainTooFew, Some("too few domains"), ); } #[test] fn test_bad_example_17() { expect_with_options( "email@[127.0.0.256]", Options::default().without_domain_literal(), Error::UnsupportedDomainLiteral, Some("unsupported domain literal (1)"), ); } #[test] fn test_bad_example_18() { expect_with_options( "email@[2001:db8::12345]", Options::default().without_domain_literal(), Error::UnsupportedDomainLiteral, Some("unsupported domain literal (2)"), ); } #[test] fn test_bad_example_19() { expect_with_options( "email@[2001:db8:0:0:0:0:1]", Options::default().without_domain_literal(), Error::UnsupportedDomainLiteral, Some("unsupported domain literal (3)"), ); } #[test] fn test_bad_example_20() { expect_with_options( "email@[::ffff:127.0.0.256]", Options::default().without_domain_literal(), Error::UnsupportedDomainLiteral, Some("unsupported domain literal (4)"), ); } // make sure Error impl Send + Sync fn is_send() {} fn is_sync() {} #[test] fn test_error_traits() { is_send::(); is_sync::(); } #[test] fn test_parse_trimmed() { let email = EmailAddress::parse_with_options( " Simons Email ", Options::default(), ) .unwrap(); assert_eq!(email.display_part(), "Simons Email"); assert_eq!(email.email(), "simon@example.com"); } #[test] // Feature test: GitHub PR: #15 fn test_parse_display_name() { let email = EmailAddress::parse_with_options( "Simons Email ", Options::default(), ) .unwrap(); assert_eq!(email.display_part(), "Simons Email"); assert_eq!(email.email(), "simon@example.com"); assert_eq!(email.local_part(), "simon"); assert_eq!(email.domain(), "example.com"); } #[test] // Feature test: GitHub PR: #15 fn test_parse_display_empty_name() { expect( "", Error::MissingDisplayName, Some("missing display name"), ); } #[test] // Feature test: GitHub PR: #15 // Reference: GitHub issue #14 fn test_parse_display_empty_name_2() { expect_with_options( "", Options::default().without_display_text(), Error::InvalidCharacter, Some("without display text '<' is invalid"), ); } #[test] // Feature test: GitHub PR: #15 // Reference: GitHub issue #14 fn test_parse_display_name_unsupported() { expect_with_options( "Simons Email ", Options::default().without_display_text(), Error::UnsupportedDisplayName, Some("unsupported display name (1)"), ); } #[test] // Regression test: GitHub issue #23 fn test_missing_tld() { EmailAddress::parse_with_options("simon@localhost", Options::default()).unwrap(); EmailAddress::parse_with_options( "simon@localhost", Options::default().with_no_minimum_sub_domains(), ) .unwrap(); expect_with_options( "simon@localhost", Options::default().with_required_tld(), Error::DomainTooFew, Some("too few domain segments"), ); } #[test] // Regression test: GitHub issue #11 fn test_eq_name_case_sensitive_local() { let email = EmailAddress::new_unchecked("simon@example.com"); assert_eq!(email, EmailAddress::new_unchecked("simon@example.com")); assert_ne!(email, EmailAddress::new_unchecked("Simon@example.com")); assert_ne!(email, EmailAddress::new_unchecked("simoN@example.com")); } #[test] // Regression test: GitHub issue #11 fn test_eq_name_case_insensitive_domain() { let email = EmailAddress::new_unchecked("simon@example.com"); assert_eq!(email, EmailAddress::new_unchecked("simon@Example.com")); assert_eq!(email, EmailAddress::new_unchecked("simon@example.COM")); } #[test] // Regression test: GitHub issue #21 fn test_utf8_non_ascii() { assert!(!is_utf8_non_ascii('A')); assert!(!is_utf8_non_ascii('§')); assert!(!is_utf8_non_ascii('�')); assert!(!is_utf8_non_ascii('\u{0F40}')); assert!(is_utf8_non_ascii('\u{C2B0}')); } }