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