Initial push
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
# Common language header
|
||||
language: rust
|
||||
cache: cargo
|
||||
dist: xenial
|
||||
|
||||
# Channels and versions I want to build
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- 1.40.0
|
||||
- nightly
|
||||
|
||||
# Operating systems I want to test
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
|
||||
# Set global environment only
|
||||
env:
|
||||
global:
|
||||
- CARGO_CI=ci
|
||||
- CARGO_DEBUG=1
|
||||
- CARGO_DEPLOY=1
|
||||
- CARGO_FLAGS=--verbose
|
||||
- CARGO_LINTER=fmt
|
||||
- secure: "SwxX4DVDM7eS3AjJhdDCAIWIYi9IMfiYIAu+n4Fx4fkPf8RwCg/h1gA4RV6WNZBpus8fzD065GHnYWP6BVllyFzoF2WYG9xBTcSA+gJHFrGSSqa7xNLEZCTUkSb0KLtVv7GbgVZgH4lHgh+BTfAYwTn/ty9bBoz+qlF9k0tRJsP7q8KsL+XSZWIQdQuuCFnjnkaNYoJPg3Iz4BDV77Wyr2hIK1bsos1NNslCUJxp3d3O16lEd0L2uU19iZ6ORgOaTq5NUHZ/mTaRFmtLgZjHHNp0YYaoZCZjNqG5PDXZm0vU7e7rprx6s9p1O9Ll6jEAQcE07tNa1Om26hsYCS4HP5+zqNrhb6d5oB6n5NjobOK3E1AamgbnJglh96lxqreOICGRHEwYurgq+IyxXFE+xGfkxsi45xcdiX0HueFxYgfjbjtB5osXhE0pci2UXMDwcNhjqPs3WZ1cCPQKjYx64Rxt1bnOHLj8BqNmk075wDtHNqzbNI5k5VK9IvajNfXnVLTghFqIccSHNOswVRAmcnH+nc83bj9XCe4JMOh0UzIngg+wRVIcvrTkxWWdVAE0m/8XAJr06ZWF2bGRK2AATJqSeonyBpYxl7kK/qJoUs8oCrcw0KIVe1/Wd7HUwLoy4zxuVxH07F0H/v231Zip3XMvz8P3/1Sfm27MuMYuZlw="
|
||||
|
||||
install:
|
||||
- git clone https://github.com/johnstonskj/rust-ci.git $CARGO_CI
|
||||
- $CARGO_CI/bin/cargo-lint.sh --install
|
||||
|
||||
jobs:
|
||||
# Performance tweak
|
||||
fast_finish: true
|
||||
# Ignore failures in nightly, not ideal, but necessary
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
- os: osx
|
||||
|
||||
# Script supports packages and workspaces
|
||||
script:
|
||||
- $CARGO_CI/bin/cargo-build.sh
|
||||
- $CARGO_CI/bin/cargo-lint.sh
|
||||
|
||||
# Deployment script, this is under test
|
||||
deploy:
|
||||
provider: script
|
||||
on:
|
||||
tags: true
|
||||
all_branches: true
|
||||
condition: "$TRAVIS_RUST_VERSION = stable && $TRAVIS_OS_NAME = linux && $CARGO_DEPLOY = 1"
|
||||
script: $CARGO_CI/bin/cargo-publish.sh
|
||||
|
||||
# Only initiate build on master or tag branches
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /\d+\.\d+(\.\d+)?(\-[a-z]+[a-zA-Z0-9]*)?/
|
||||
|
||||
# Suppress at least some emails
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "email_address"
|
||||
version = "0.1.0"
|
||||
authors = ["Simon Johnston <johnstonskj@gmail.com>"]
|
||||
description = "Provides a new type for validation of email addresses"
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
publish = true
|
||||
|
||||
[features]
|
||||
default = ["serde_support"]
|
||||
serde_support = ["serde"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
# Optional dependencies
|
||||
serde = { optional = true, version = "1.0", features = ["derive"] }
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT 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.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Crate email_address
|
||||
|
||||
A Rust crate providing an implementation of an RFC-compliant `EmailAddress` newtype.
|
||||
|
||||

|
||||

|
||||
[](https://crates.io/crates/email_address)
|
||||
[](https://docs.rs/email_address)
|
||||
[](https://travis-ci.org/johnstonskj/rust-email_address)
|
||||
[](https://github.com/johnstonskj/rust-email_address/stargazers)
|
||||
|
||||
TBD
|
||||
|
||||
## 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 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 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 5234: [_Augmented BNF for Syntax Specifications: ABNF_](https://tools.ietf.org/html/rfc5234),
|
||||
IETF, Jan 2008.
|
||||
|
||||
## Changes
|
||||
|
||||
**Version 0.1.0**
|
||||
|
||||
* Basic type implementation and structure based on RFC 5322.
|
||||
* Only supported for ASCII.
|
||||
* Does not parse comments.
|
||||
|
||||
## TODO
|
||||
|
||||
1. Full Unicode support.
|
||||
1. Support comments.
|
||||
1. Support line-feed and whitespace rules.
|
||||
+609
@@ -0,0 +1,609 @@
|
||||
/*!
|
||||
A Rust crate providing an implementation of an RFC-compliant `EmailAddress` newtype.
|
||||
|
||||
# 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 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 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 5234: [_Augmented BNF for Syntax Specifications: ABNF_](https://tools.ietf.org/html/rfc5234),
|
||||
IETF, Jan 2008.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
|
||||
```
|
||||
|
||||
This refers 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).
|
||||
|
||||
# Example
|
||||
|
||||
*/
|
||||
|
||||
#![warn(
|
||||
missing_debug_implementations,
|
||||
missing_docs,
|
||||
unused_extern_crates,
|
||||
rust_2018_idioms
|
||||
)]
|
||||
|
||||
#[cfg(feature = "serde_support")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Public Types
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Error {
|
||||
InvalidCharacter,
|
||||
LocalPartEmpty,
|
||||
LocalPartTooLong,
|
||||
DomainEmpty,
|
||||
DomainTooLong,
|
||||
MissingSeparator,
|
||||
TooManySeparators,
|
||||
DomainTooFew,
|
||||
DomainInvalidSeparator,
|
||||
InvalidIPAddress,
|
||||
UnbalancedQuotes,
|
||||
InvalidComment,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
|
||||
pub struct EmailAddress(String);
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Implementations
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
const LOCAL_PART_MAX_LENGTH: usize = 64;
|
||||
const DOMAIN_MAX_LENGTH: usize = 255;
|
||||
|
||||
const CR: char = '\r';
|
||||
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 = ']';
|
||||
const LPAREN: char = '(';
|
||||
const RPAREN: char = ')';
|
||||
const LT: char = '<';
|
||||
const GT: char = '>';
|
||||
|
||||
const IPV6_PREFIX: &str = "IPv6:";
|
||||
|
||||
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::MissingSeparator => write!(f, "Missing separator character '{}'.", AT),
|
||||
Error::TooManySeparators => {
|
||||
write!(f, "Found more than one 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 doamin."),
|
||||
Error::UnbalancedQuotes => write!(f, "Quotes around the local-part are unbalanced."),
|
||||
Error::InvalidComment => write!(f, "A comment was badly formed."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for Error {}
|
||||
|
||||
unsafe impl Sync for Error {}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl<T> Into<std::result::Result<T, Error>> for Error {
|
||||
fn into(self) -> Result<T, Error> {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
impl Display for EmailAddress {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EmailAddress {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
parse_address(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailAddress {
|
||||
pub fn is_valid(address: &str) -> bool {
|
||||
Self::from_str(&address).is_ok()
|
||||
}
|
||||
|
||||
pub fn from(local_part: &str, domain: &str) -> Result<Self, Error> {
|
||||
Self::from_str(&format!("{}{}{}", local_part, AT, domain))
|
||||
}
|
||||
|
||||
pub fn to_uri(&self) -> String {
|
||||
format!("{}{}", MAILTO_URI_PREFIX, self)
|
||||
}
|
||||
|
||||
pub fn to_display(&self, name: String) -> String {
|
||||
format!("{} <{}>", name, self)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Private Functions
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
fn parse_address(address: &str) -> Result<EmailAddress, Error> {
|
||||
let address = if address.starts_with(LT) && address.ends_with(GT) {
|
||||
&address[1..address.len() - 1]
|
||||
} else {
|
||||
address
|
||||
};
|
||||
let parts = address.split(AT).collect::<Vec<&str>>();
|
||||
match parts.len() {
|
||||
l if l < 2 => Error::MissingSeparator.into(),
|
||||
l if l > 2 => Error::TooManySeparators.into(),
|
||||
_ => {
|
||||
parse_local_part(parts.first().unwrap())?;
|
||||
parse_domain(parts.last().unwrap())?;
|
||||
Ok(EmailAddress(address.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_local_part(part: &str) -> 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) {
|
||||
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 part.is_ascii() && is_qcontent(part) {
|
||||
return Ok(());
|
||||
}
|
||||
Error::InvalidCharacter.into()
|
||||
}
|
||||
|
||||
fn parse_unquoted_local_part(part: &str) -> Result<(), Error> {
|
||||
if part.is_ascii() && is_dot_atom_text(part) {
|
||||
return Ok(());
|
||||
}
|
||||
Error::InvalidCharacter.into()
|
||||
}
|
||||
|
||||
fn parse_domain(part: &str) -> Result<(), Error> {
|
||||
if part.is_empty() {
|
||||
Error::DomainEmpty.into()
|
||||
} else if part.len() > LOCAL_PART_MAX_LENGTH {
|
||||
Error::DomainTooLong.into()
|
||||
} else if part.starts_with(LBRACKET) && part.ends_with(RBRACKET) {
|
||||
parse_literal_domain(&part[1..part.len() - 1])
|
||||
} else {
|
||||
parse_text_domain(part)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_text_domain(part: &str) -> Result<(), Error> {
|
||||
if part.is_ascii() && is_dot_atom_text(part) {
|
||||
return Ok(());
|
||||
}
|
||||
Error::InvalidCharacter.into()
|
||||
}
|
||||
|
||||
fn parse_literal_domain(part: &str) -> Result<(), Error> {
|
||||
if part.is_ascii() && 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 == '~'
|
||||
}
|
||||
|
||||
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 {
|
||||
c >= '\x21' && c <= '\x7E'
|
||||
}
|
||||
|
||||
fn is_wsp(c: char) -> bool {
|
||||
c == SP || c == HTAB
|
||||
}
|
||||
|
||||
fn is_ctext_char(c: char) -> bool {
|
||||
(c >= '\x21' && c == '\x27') || (c >= '\x2A' && c <= '\x5B') || (c >= '\x5D' && c <= '\x7E')
|
||||
}
|
||||
|
||||
fn is_qtext_char(c: char) -> bool {
|
||||
c == '\x21' || (c >= '\x23' && c <= '\x5B') || (c >= '\x5D' && c <= '\x7E')
|
||||
}
|
||||
|
||||
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 {
|
||||
(c >= '\x21' && c <= '\x5A') || (c >= '\x5E' && c <= '\x7E')
|
||||
}
|
||||
|
||||
fn is_ctext(s: &str) -> bool {
|
||||
s.chars().all(is_ctext_char)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Unit Tests
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_01() {
|
||||
assert!(EmailAddress::is_valid("simple@example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_02() {
|
||||
assert!(EmailAddress::is_valid("very.common@example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_03() {
|
||||
assert!(EmailAddress::is_valid(
|
||||
"disposable.style.email.with+symbol@example.com"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_04() {
|
||||
assert!(EmailAddress::is_valid(
|
||||
"other.email-with-hyphen@example.com"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_05() {
|
||||
assert!(EmailAddress::is_valid("fully-qualified-domain@example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_06() {
|
||||
// may go to user.name@example.com inbox depending on mail server
|
||||
assert!(EmailAddress::is_valid("user.name+tag+sorting@example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_07() {
|
||||
// one-letter local-part
|
||||
assert!(EmailAddress::is_valid("x@example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_08() {
|
||||
assert!(EmailAddress::is_valid("example-indeed@strange-example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_09() {
|
||||
// local domain name with no TLD, although ICANN highly discourages dotless email addresses
|
||||
assert!(EmailAddress::is_valid("admin@mailserver1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_10() {
|
||||
// see the List of Internet top-level domains
|
||||
assert!(EmailAddress::is_valid("example@s.example"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_11() {
|
||||
// space between the quotes
|
||||
assert!(EmailAddress::is_valid("\" \"@example.org"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_12() {
|
||||
// quoted double dot
|
||||
assert!(EmailAddress::is_valid("\"john..doe\"@example.org"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_13() {
|
||||
// bangified host route used for uucp mailers
|
||||
assert!(EmailAddress::is_valid("mailhost!username@example.org"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_14() {
|
||||
// % escaped mail route to user@example.com via example.org
|
||||
assert!(EmailAddress::is_valid("user%example.com@example.org"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_15() {
|
||||
assert!(EmailAddress::is_valid("jsmith@[192.168.2.1]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_good_examples_from_wikipedia_16() {
|
||||
assert!(EmailAddress::is_valid("jsmith@[IPv6:2001:db8::1]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_examples_from_wikipedia_00() {
|
||||
// (no @ character)
|
||||
assert_eq!(
|
||||
EmailAddress::from_str("Abc.example.com"),
|
||||
Error::MissingSeparator.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_examples_from_wikipedia_01() {
|
||||
// (only one @ is allowed outside quotation marks)
|
||||
assert_eq!(
|
||||
EmailAddress::from_str("A@b@c@example.com"),
|
||||
Error::TooManySeparators.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_examples_from_wikipedia_02() {
|
||||
// (none of the special characters in this local-part are allowed outside quotation marks)
|
||||
assert_eq!(
|
||||
EmailAddress::from_str("a\"b(c)d,e:f;g<h>i[j\\k]l@example.com"),
|
||||
Error::InvalidCharacter.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_examples_from_wikipedia_03() {
|
||||
// (quoted strings must be dot separated or the only element making up the local-part)
|
||||
assert_eq!(
|
||||
EmailAddress::from_str("just\"not\"right@example.com"),
|
||||
Error::InvalidCharacter.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_examples_from_wikipedia_04() {
|
||||
// (spaces, quotes, and backslashes may only exist when within quoted strings and preceded by a backslash)
|
||||
assert_eq!(
|
||||
EmailAddress::from_str("this is\"not\\allowed@example.com"),
|
||||
Error::InvalidCharacter.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_examples_from_wikipedia_05() {
|
||||
// (even if escaped (preceded by a backslash), spaces, quotes, and backslashes must still be contained by quotes)
|
||||
assert_eq!(
|
||||
EmailAddress::from_str("this\\ still\"not\\allowed@example.com"),
|
||||
Error::InvalidCharacter.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_examples_from_wikipedia_06() {
|
||||
// (local part is longer than 64 characters)
|
||||
assert_eq!(
|
||||
EmailAddress::from_str(
|
||||
"1234567890123456789012345678901234567890123456789012345678901234+x@example.com"
|
||||
),
|
||||
Error::LocalPartTooLong.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user