├── .drone.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── shard.yml ├── spec ├── CrystalEmail_spec.cr └── spec_helper.cr └── src ├── CrystalEmail.cr └── CrystalEmail ├── core.cr ├── rfc1123.cr ├── rfc1123 ├── public.cr ├── public │ └── string.cr └── string.cr ├── rfc5322.cr ├── rfc5322 ├── public.cr ├── public │ └── string.cr └── string.cr └── version.cr /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | steps: 5 | - name: test 6 | image: crystallang/crystal:latest 7 | environment: 8 | commands: 9 | - crystal spec 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | /.crystal/ 4 | /.shards/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A RFC compliant Email validator 2 | 3 | [![Build Status](https://drone.sceptique.eu/api/badges/Sceptique/CrystalEmail/status.svg)](https://drone.sceptique.eu/Sceptique/CrystalEmail) 4 | 5 | **Migrated to ** 6 | 7 | #### Prelude 8 | - What is an [Email Address](https://en.wikipedia.org/wiki/Email_address) ? 9 | - What is a [Domain Name](https://en.wikipedia.org/wiki/Hostname) ? 10 | 11 | #### Compliance 12 | - Compliant to the [Rfc 5322](https://tools.ietf.org/html/rfc5322). 13 | - Compliant to the [Rfc 1123](https://tools.ietf.org/html/rfc1123). 14 | 15 | #### To do 16 | - To do : [rfc 6530](https://tools.ietf.org/html/rfc6530). 17 | - To do : Implement IPv6 18 | 19 | #### Notes 20 | - No ipv6 for now 21 | - No escaped characters 22 | - Public email validity (no raw ip, domain withour root domain, ...) 23 | 24 | 25 | ## Installation 26 | 27 | Tested with crystal 0.17 - 1.1.0 28 | 29 | Add this to your application's `shard.yml`: 30 | 31 | ```yaml 32 | dependencies: 33 | CrystalEmail: 34 | git: https://git.sceptique.eu/Sceptique/CrystalEmail 35 | branch: master 36 | ``` 37 | 38 | ## Usage in Crystal 39 | 40 | ```crystal 41 | require "CrystalEmail" 42 | 43 | # Pure Rfc5322 44 | # this is what you want if you need to allow local domains 45 | CrystalEmail::Rfc5322.validates? "toto@tata" # => true 46 | CrystalEmail::Rfc5322.match "toto@tata" # => # 47 | CrystalEmail::Rfc5322.validates? "toto" # => false 48 | CrystalEmail::Rfc5322.match "toto" # => nil 49 | 50 | # Rfc5322 + Internet basic usage 51 | # most of the website on internet will require a domain like "domain.thing" 52 | CrystalEmail::Rfc5322::Public.validates? "toto@tata.com" # => true 53 | CrystalEmail::Rfc5322::Public.match "toto@tata.com" # => # 54 | 55 | "toto@toto.toto".is_email? # => true 56 | ``` 57 | 58 | 59 | ## Contributes ! 60 | 61 | Find a bug ? Want a new feature ? 62 | Create a clear pull request and we'll see :) 63 | 64 | - Sceptique 65 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: CrystalEmail 2 | version: 0.2.6 3 | crystal: ">= 1.0.0" 4 | 5 | authors: 6 | - Arthur Poulet 7 | 8 | license: WTFPL 9 | 10 | -------------------------------------------------------------------------------- /spec/CrystalEmail_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | require "../src/CrystalEmail" 3 | 4 | describe CrystalEmail do 5 | it "works" do 6 | CrystalEmail::Rfc5322.validates?("toto@tata").should eq(true) 7 | CrystalEmail::Rfc5322.validates?("toto@tata.tata").should eq(true) 8 | CrystalEmail::Rfc5322.validates?("a.b@dom").should eq(true) 9 | CrystalEmail::Rfc5322.validates?("a.b.c@dom.ex.fe").should eq(true) 10 | CrystalEmail::Rfc5322.validates?("-@x").should eq(true) 11 | CrystalEmail::Rfc5322.validates?("u@1.1.1.1").should eq(true) 12 | CrystalEmail::Rfc5322.validates?("u@a-a").should eq(true) 13 | end 14 | 15 | it "does not works" do 16 | CrystalEmail::Rfc5322.validates?("toto@").should eq(false) # no domain 17 | CrystalEmail::Rfc5322.validates?("@tata.tata").should eq(false) # no user 18 | CrystalEmail::Rfc5322.validates?("a.b.dom").should eq(false) # no @ 19 | CrystalEmail::Rfc5322.validates?("a.b.c@dom.ex.1").should eq(false) # first dom has to begin with a alpha 20 | CrystalEmail::Rfc5322.validates?("u@256.1.1.1").should eq(false) # not a valid ip 21 | CrystalEmail::Rfc5322.validates?("u@-a").should eq(false) # not a valid dom 22 | CrystalEmail::Rfc5322.validates?("u@-a.a").should eq(false) # not a valid dom 23 | end 24 | 25 | it "test public" do 26 | CrystalEmail::Rfc1123::Public.validates?("toto.toto.to").should eq(true) 27 | CrystalEmail::Rfc1123::Public.validates?("toto.to").should eq(true) 28 | CrystalEmail::Rfc1123::Public.validates?("toto").should eq(false) 29 | CrystalEmail::Rfc1123::Public.validates?("toto.t").should eq(false) 30 | CrystalEmail::Rfc1123::Public.validates?("toto.toto.t").should eq(false) 31 | CrystalEmail::Rfc1123::Public.validates?("1.1.1.1").should eq(false) 32 | 33 | CrystalEmail::Rfc5322::Public.validates?("toto@toto.to").should eq(true) 34 | CrystalEmail::Rfc5322::Public.validates?("toto@toto").should eq(false) 35 | CrystalEmail::Rfc5322::Public.validates?("toto@1.1.1.1").should eq(false) 36 | end 37 | 38 | it "test string helpers" do 39 | ("*".is_domain?).should eq(false) 40 | ("toto.toto".is_domain?).should eq(true) 41 | ("toto".is_public_domain?).should eq(false) 42 | ("toto.toto".is_public_domain?).should eq(true) 43 | ("toto".is_email?).should eq(false) 44 | ("toto@toto.toto".is_email?).should eq(true) 45 | ("toto@toto".is_public_email?).should eq(false) 46 | ("toto@toto.toto".is_public_email?).should eq(true) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/CrystalEmail" 3 | -------------------------------------------------------------------------------- /src/CrystalEmail.cr: -------------------------------------------------------------------------------- 1 | require "./CrystalEmail/core" 2 | require "./CrystalEmail/rfc1123" 3 | require "./CrystalEmail/rfc1123/string" 4 | require "./CrystalEmail/rfc1123/public" 5 | require "./CrystalEmail/rfc1123/public/string" 6 | require "./CrystalEmail/rfc5322" 7 | require "./CrystalEmail/rfc5322/string" 8 | require "./CrystalEmail/rfc5322/public" 9 | require "./CrystalEmail/rfc5322/public/string" 10 | 11 | module CrystalEmail 12 | end 13 | 14 | # puts CrystalEmail::Rfc5322.match("toto@toto.toto") 15 | -------------------------------------------------------------------------------- /src/CrystalEmail/core.cr: -------------------------------------------------------------------------------- 1 | module CrystalEmail 2 | # Abstract class to inherit and complete by adding the REGEXP constant 3 | class Core 4 | # Check if the {::String} is a valid email. 5 | # @param str [::String] string to match 6 | # @raise [ArgumentError] if str is not a String 7 | # @return [TrueClass or FalseClass] 8 | def self.validates?(str : String) : Bool 9 | !!match(str) 10 | end 11 | 12 | # Check if the string is a valid email and details how 13 | # @param str [::String] string to match 14 | # @raise [ArgumentError] if str is not a String 15 | # @return [MatchData or NilClass] matched email with the keys "local" and "domain" 16 | def self.match(str : String) 17 | raise ArgumentError.new "Cannot validate a `#{str.class}`. Only `String` can be." unless str.is_a?(String) 18 | str.match regexp 19 | end 20 | 21 | # @return [Regexp] to erase in the children 22 | def self.regexp 23 | raise NoMethodError.new "Not implemented in #{self.class}" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /src/CrystalEmail/rfc1123.cr: -------------------------------------------------------------------------------- 1 | # require "./core" 2 | 3 | module CrystalEmail 4 | # a Domain Name follows the Rfc1123: https://tools.ietf.org/html/rfc1123 5 | class Rfc1123 < Core 6 | # one valid character for domain part (first name first character) 7 | ATEXT_FIRST_FIRST = "([A-Za-z])" 8 | # one valid character for domain part (first name all other characters) 9 | ATEXT_FIRST_ALL = "([A-Za-z0-9])" 10 | # one valid character for domain part (all other names) 11 | ATEXT_ALL = "([A-Za-z0-9\-])" 12 | 13 | # a valid string for domain part (first name) 14 | ATOM_FIRST = "#{ATEXT_FIRST_FIRST}#{ATEXT_ALL}{0,62}" 15 | # a valid string for domain part (all other names) 16 | ATOM_ALL = "#{ATEXT_FIRST_ALL}#{ATEXT_ALL}{0,62}" 17 | 18 | # a valid string with subdomains, separated by dots for domain part as IPV4 19 | IPV4 = "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" 20 | # a valid string with subdomains, separated by dots for domain part as Domain Name 21 | DOT_ATOM_TEXT = "((#{ATOM_ALL}\\.)*#{ATOM_FIRST})" 22 | 23 | # email grammar 24 | VALIDE = "(?(?!.{254,})((#{DOT_ATOM_TEXT})|(#{IPV4})))" 25 | 26 | # regexp to validate complete email 27 | REGEXP = /\A#{VALIDE}\Z/ 28 | 29 | def self.regexp 30 | return REGEXP 31 | end 32 | 33 | module String 34 | # Check if the current [::String] instance is a valid domain 35 | # @return [TrueClass or FalseClass] 36 | def is_domain? : Bool 37 | CrystalEmail::Rfc1123.validates? self 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/CrystalEmail/rfc1123/public.cr: -------------------------------------------------------------------------------- 1 | require "../rfc1123" 2 | 3 | module CrystalEmail 4 | class Rfc1123 5 | # Internet realist version of {Rfc5322}. It requires 2 names (a.b). 6 | class Public < Rfc1123 7 | # !! changes with the basi rfc ({1,62} ATEX_ALL instead of {0,62}) 8 | # a valid string for domain part (first name) 9 | ATOM_FIRST = "#{ATEXT_FIRST_FIRST}#{ATEXT_ALL}{1,62}" 10 | 11 | # !! changes with the basi rfc (ATOM_ALL+ instead of ATOM_ALL*) 12 | # a valid string with subdomains, separated by dots for domain part as Domain Name 13 | DOT_ATOM_TEXT = "((#{ATOM_ALL}\\.)+#{ATOM_FIRST})" 14 | 15 | # email grammar 16 | VALIDE = "(?(?!.{254,})((#{DOT_ATOM_TEXT})))" 17 | REGEXP = /\A#{VALIDE}\Z/ 18 | 19 | def self.regexp 20 | return REGEXP 21 | end 22 | 23 | module String 24 | # Check if the current [::String] instance is a valid domain on internet 25 | # @return [TrueClass or FalseClass] 26 | def is_public_domain? 27 | CrystalEmail::Rfc1123::Public.validates? self 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/CrystalEmail/rfc1123/public/string.cr: -------------------------------------------------------------------------------- 1 | require "../public" 2 | 3 | class String 4 | include CrystalEmail::Rfc1123::Public::String 5 | end 6 | -------------------------------------------------------------------------------- /src/CrystalEmail/rfc1123/string.cr: -------------------------------------------------------------------------------- 1 | require "../rfc1123" 2 | 3 | class String 4 | include CrystalEmail::Rfc1123::String 5 | end 6 | -------------------------------------------------------------------------------- /src/CrystalEmail/rfc5322.cr: -------------------------------------------------------------------------------- 1 | # require "core" 2 | # require "rfc1123" 3 | 4 | module CrystalEmail 5 | # an Email Address follows the Rfc5322: http://www.ietf.org/rfc/rfc5322.txt 6 | # a Domain Name follows the Rfc1123: https://tools.ietf.org/html/rfc1123 7 | # The Rfc5322 designate an email with the format local@domain, where 8 | #  - local is a string of US-ASCII characters, without some symboles. 9 | # - domain is a valid Domain Name 10 | class Rfc5322 < Core 11 | # one valid character for local part (do not take escaped because the Rfc prefere to avoid them) 12 | ATEXT = "([A-Za-z0-9!#\\$%&\\'*\\+\\-/=\\?\\^_`\\{\\}\\|~])" 13 | 14 | # a valid string for local part 15 | ATOM = "#{ATEXT}+" 16 | 17 | # a valid string with subdomains, separated by dots for local part 18 | DOT_ATOM_TEXT = "(#{ATOM})(\\.#{ATOM})*" 19 | 20 | # email grammar 21 | VALIDE = "(?#{DOT_ATOM_TEXT})@(#{Rfc1123::VALIDE})" 22 | 23 | # regexp to validate complete email 24 | REGEXP = /\A#{VALIDE}\Z/ 25 | 26 | def self.regexp 27 | return REGEXP 28 | end 29 | 30 | module String 31 | # Check if the current [::String] instance is a valid email 32 | # @return [TrueClass or FalseClass] 33 | def is_email? : Bool 34 | CrystalEmail::Rfc5322.validates? self 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /src/CrystalEmail/rfc5322/public.cr: -------------------------------------------------------------------------------- 1 | require "../rfc5322" 2 | require "../rfc1123/public" 3 | 4 | module CrystalEmail 5 | class Rfc5322 6 | # Internet realist version of {Rfc5322}. It requires a root domain. 7 | class Public < Rfc5322 8 | VALIDE = "(?#{DOT_ATOM_TEXT})@(#{Rfc1123::Public::VALIDE})" 9 | REGEXP = /\A#{VALIDE}\Z/ 10 | 11 | def self.regexp 12 | return REGEXP 13 | end 14 | 15 | module String 16 | # Check if the current [::String] instance is a valid email 17 | # @return [TrueClass or FalseClass] 18 | def is_public_email? 19 | CrystalEmail::Rfc5322::Public.validates? self 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/CrystalEmail/rfc5322/public/string.cr: -------------------------------------------------------------------------------- 1 | require "../public" 2 | 3 | class String 4 | include CrystalEmail::Rfc5322::Public::String 5 | end 6 | -------------------------------------------------------------------------------- /src/CrystalEmail/rfc5322/string.cr: -------------------------------------------------------------------------------- 1 | require "../rfc5322" 2 | 3 | class String 4 | include CrystalEmail::Rfc5322::String 5 | end 6 | -------------------------------------------------------------------------------- /src/CrystalEmail/version.cr: -------------------------------------------------------------------------------- 1 | module CrystalEmail 2 | VERSION = "0.2.4" 3 | end 4 | --------------------------------------------------------------------------------