diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ef243b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +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`. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..68f8467 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,28 @@ +--- +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. diff --git a/.github/ISSUE_TEMPLATE/spec_bug_report.md b/.github/ISSUE_TEMPLATE/spec_bug_report.md new file mode 100644 index 0000000..0b6f87f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/spec_bug_report.md @@ -0,0 +1,27 @@ +--- +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. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9f329cd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..204cfcb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# 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 --all-features -- --exact + +Make your change. Add tests, and documentation, for your change. Ensure not only that tests pass, but the following all run successfully. + + cargo doc --all-features --no-deps + cargo fmt + cargo clippy + +If you made changes to the book source, ensure the following runs successfully + + mdbook build + +If you have made any changes to `Cargo.toml`, also check: + + cargo outdated + 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. diff --git a/Cargo.toml b/Cargo.toml index cd7c5e6..ee0feea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "email_address" -version = "0.2.3" +version = "0.2.4" authors = ["Simon Johnston "] description = "A Rust crate providing an implementation of an RFC-compliant `EmailAddress` newtype. " documentation = "https://docs.rs/email_address/" diff --git a/README.md b/README.md index 83174f9..5cbb741 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,14 @@ assert_eq!( ## Changes +**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)). diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 0000000..be6302c --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,38 @@ +# Description + +Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A +- [ ] Test B + +**Environment (please complete the following information):** + - Platform: [e.g.`uname -a`] + - Rust [e.g.`rustic -vV`] + - Cargo [e.g.`cargo -vV`] + +# Checklist: + +- [ ] My code follows the style guidelines of this project (e.g. run `cargo fmt`) +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] My changes DO NOT require unstable features without prior agreement +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/src/lib.rs b/src/lib.rs index 2569f64..8990608 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,6 +282,7 @@ An informal description can be found on [Wikipedia](https://en.wikipedia.org/wik use serde::{Deserialize, Serialize}; use std::fmt; use std::fmt::{Debug, Display, Formatter}; +use std::hash::Hash; use std::str::FromStr; // ------------------------------------------------------------------------------------------------ @@ -305,6 +306,8 @@ pub enum Error { 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`. @@ -317,6 +320,33 @@ pub enum Error { InvalidComment, /// An IP address in a `domain-literal` was malformed. InvalidIPAddress, + /// A `domain-literal` was supplied, but is unsupported by parser configuration. + UnsupportedDomainLiteral, +} + +/// +/// 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. + /// + pub minimum_sub_domains: usize, + + /// + /// Specifies if domain literals are allowed. Defaults to `true`. + /// + pub allow_domain_literal: bool, +} + +impl Default for Options { + fn default() -> Self { + Self { + minimum_sub_domains: Default::default(), + allow_domain_literal: true, + } + } } /// @@ -325,7 +355,7 @@ pub enum Error { /// create an instance. The various components of the email _are not_ parsed out to be accessible /// independently. /// -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Eq)] #[cfg_attr(feature = "serde_support", derive(Serialize))] pub struct EmailAddress(String); @@ -373,6 +403,7 @@ impl Display for Error { 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: {}", @@ -386,6 +417,7 @@ impl Display for Error { 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."), } } } @@ -406,11 +438,35 @@ impl Display for EmailAddress { } } +// 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 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) + parse_address(s, Default::default()) } } @@ -474,8 +530,27 @@ impl EmailAddress { /// /// assert_eq!("John Doe ", email.to_display("John Doe")); /// ``` - pub fn new_unchecked(address: String) -> Self { - Self(address) + 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 }; + /// 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) } /// @@ -498,7 +573,7 @@ impl EmailAddress { /// email address. /// pub fn is_valid_local_part(part: &str) -> bool { - parse_local_part(part).is_ok() + parse_local_part(part, Default::default()).is_ok() } /// @@ -506,7 +581,7 @@ impl EmailAddress { /// email address. /// pub fn is_valid_domain(part: &str) -> bool { - parse_domain(part).is_ok() + parse_domain(part, Default::default()).is_ok() } /// @@ -584,6 +659,13 @@ impl EmailAddress { let (_, right) = split_at(&self.0).unwrap(); right } + + /// + /// Returns the email address as a string reference. + /// + pub fn as_str(&self) -> &str { + self.as_ref() + } } // ------------------------------------------------------------------------------------------------ @@ -624,14 +706,14 @@ fn is_uri_reserved(c: char) -> bool { || c == ']' } -fn parse_address(address: &str) -> Result { +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 (left, right) = split_at(address)?; - parse_local_part(left)?; - parse_domain(right)?; + parse_local_part(left, options)?; + parse_domain(right, options)?; Ok(EmailAddress(address.to_owned())) } @@ -642,7 +724,7 @@ fn split_at(address: &str) -> Result<(&str, &str), Error> { } } -fn parse_local_part(part: &str) -> Result<(), Error> { +fn parse_local_part(part: &str, _options: Options) -> Result<(), Error> { if part.is_empty() { Error::LocalPartEmpty.into() } else if part.len() > LOCAL_PART_MAX_LENGTH { @@ -674,28 +756,57 @@ fn parse_unquoted_local_part(part: &str) -> Result<(), Error> { Error::InvalidCharacter.into() } -fn parse_domain(part: &str) -> Result<(), Error> { +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) { - parse_literal_domain(&part[1..part.len() - 1]) + if options.allow_domain_literal { + parse_literal_domain(&part[1..part.len() - 1]) + } else { + Error::UnsupportedDomainLiteral.into() + } } else { - parse_text_domain(part) + parse_text_domain(part, options) } } -fn parse_text_domain(part: &str) -> Result<(), Error> { - if is_dot_atom_text(part) { - for sub_part in part.split(DOT) { - if sub_part.len() > SUB_DOMAIN_MAX_LENGTH { - return Error::SubDomainTooLong.into(); - } +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(); } - return Ok(()); + // 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(()) } - Error::InvalidCharacter.into() } fn parse_literal_domain(part: &str) -> Result<(), Error> { @@ -821,6 +932,16 @@ mod tests { 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); @@ -964,6 +1085,102 @@ mod tests { 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] @@ -1006,6 +1223,19 @@ mod tests { 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( @@ -1106,6 +1336,174 @@ mod tests { 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 { + minimum_sub_domains: 2, + ..Default::default() + }, + 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 { + minimum_sub_domains: 10, + ..Default::default() + }, + Error::DomainTooFew, + Some("too few domains"), + ); + } + + #[test] + fn test_bad_example_17() { + expect_with_options( + "email@[127.0.0.256]", + Options { + allow_domain_literal: false, + ..Default::default() + }, + Error::UnsupportedDomainLiteral, + Some("unsupported domain literal (1)"), + ); + } + + #[test] + fn test_bad_example_18() { + expect_with_options( + "email@[2001:db8::12345]", + Options { + allow_domain_literal: false, + ..Default::default() + }, + 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 { + allow_domain_literal: false, + ..Default::default() + }, + Error::UnsupportedDomainLiteral, + Some("unsupported domain literal (3)"), + ); + } + + #[test] + fn test_bad_example_20() { + expect_with_options( + "email@[::ffff:127.0.0.256]", + Options { + allow_domain_literal: false, + ..Default::default() + }, + Error::UnsupportedDomainLiteral, + Some("unsupported domain literal (4)"), + ); + } + // make sure Error impl Send + Sync fn is_send() {} fn is_sync() {} @@ -1115,4 +1513,23 @@ mod tests { is_send::(); is_sync::(); } + + #[test] + // BUG: https://github.com/johnstonskj/rust-email_address/issues/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] + // BUG: https://github.com/johnstonskj/rust-email_address/issues/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")); + } }