├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── package.json ├── LICENSE.txt ├── index.js ├── README.md └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to sane-email-validation 2 | 3 | As an open source project, sane-email-validation welcomes contributions of many forms. 4 | 5 | ## Bug reporting 6 | 7 | The bug tracker is currently [hosted on GitHub](https://github.com/scottgonzalez/sane-email-validation/issues). 8 | 9 | Please search through the existing issues prior to filing a new issue. 10 | 11 | ## New features 12 | 13 | All new features should start as a discussion in the [issue tracker](https://github.com/scottgonzalez/sane-email-validation/issues). 14 | 15 | ## Patch submission 16 | 17 | Patches are welcome as [pull requests on GitHub](https://github.com/scottgonzalez/sane-email-validation/pulls). -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sane-email-validation", 3 | "version": "3.0.1", 4 | "author": "Scott González (http://scottgonzalez.com)", 5 | "description": "Sanely validate email addresses, based on HTML5's definition of email addresses", 6 | "keywords": [ 7 | "email", 8 | "validation", 9 | "validate" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/scottgonzalez/sane-email-validation.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/scottgonzalez/sane-email-validation/issues" 18 | }, 19 | "homepage": "https://github.com/scottgonzalez/sane-email-validation", 20 | "scripts": { 21 | "lint": "standard", 22 | "mocha": "mocha", 23 | "test": "npm run lint && npm run mocha" 24 | }, 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "mocha": "^9.0.3", 28 | "standard": "^16.0.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright Sane Email Validation contributors 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/scottgonzalez/sane-email-validation. 6 | 7 | ==== 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 24 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 26 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Unicode ranges from RFC 3987, section 2.2 2 | const unicodeRanges = [ 3 | '\u00A0-\uD7FF', 4 | '\uF900-\uFDCF', 5 | '\uFDF0-\uFFEF', 6 | '\u10000-\u1FFFD', 7 | '\u20000-\u2FFFD', 8 | '\u30000-\u3FFFD', 9 | '\u40000-\u4FFFD', 10 | '\u50000-\u5FFFD', 11 | '\u60000-\u6FFFD', 12 | '\u70000-\u7FFFD', 13 | '\u80000-\u8FFFD', 14 | '\u90000-\u9FFFD', 15 | '\uA0000-\uAFFFD', 16 | '\uB0000-\uBFFFD', 17 | '\uC0000-\uCFFFD', 18 | '\uD0000-\uDFFFD', 19 | '\uE1000-\uEFFFD' 20 | ].join('') 21 | 22 | const methods = [ 23 | { 24 | name: 'isEmail', 25 | characters: unicodeRanges, 26 | test: () => true 27 | }, 28 | { 29 | name: 'isAsciiEmail', 30 | characters: '', 31 | test: (localAddr, domain) => { 32 | const labels = domain.split('.') 33 | for (const label of labels) { 34 | if (label.indexOf('xn--') === 0) { 35 | return false 36 | } 37 | } 38 | 39 | return true 40 | } 41 | } 42 | ].reduce((methods, { characters, name, test }) => { 43 | const localAddrRegex = new RegExp(`^[a-z0-9.!#$%&'*+/=?^_\`{|}~${characters}-]+$`, 'i') 44 | const label = `[a-z0-9${characters}](?:[a-z0-9${characters}-]{0,61}[a-z0-9${characters}])?` 45 | const domainRegex = new RegExp(`^${label}(?:\\.${label})+$`, 'i') 46 | 47 | methods[name] = (string) => { 48 | const parts = string.split('@') 49 | 50 | if (parts.length !== 2) { 51 | return false 52 | } 53 | 54 | const [localAddr, domain] = parts 55 | 56 | if (!localAddrRegex.test(localAddr)) { 57 | return false 58 | } 59 | 60 | if (!domainRegex.test(domain)) { 61 | return false 62 | } 63 | 64 | if (localAddr.substr(-1) === '.') { 65 | return false 66 | } 67 | 68 | return test(localAddr, domain) 69 | } 70 | 71 | return methods 72 | }, {}) 73 | 74 | exports = module.exports = methods.isEmail 75 | exports.isAsciiEmail = methods.isAsciiEmail 76 | 77 | exports.isNotEmail = (string) => { 78 | return !methods.isEmail(string) 79 | } 80 | 81 | exports.isNotAsciiEmail = (string) => { 82 | return !methods.isAsciiEmail(string) 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sane Email Validation 2 | 3 | There are lots of email address validation modules, but none of them seem to check for sane email addresses. This one does. If you don't agree, please [file an issue](https://github.com/scottgonzalez/sane-email-validation/issues/new) and convince me why. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install sane-email-validation 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | const isEmail = require('sane-email-validation') 15 | const email = '...' 16 | 17 | if (isEmail(email)) { 18 | console.log(`${email} is valid.`) 19 | } else { 20 | console.log(`${email} is not valid.`) 21 | } 22 | ``` 23 | 24 | ### `isNotEmail()` 25 | 26 | An inverted check is also exposed. 27 | 28 | ```js 29 | const isNotEmail = require('sane-email-validation').isNotEmail 30 | const email = '...' 31 | 32 | if (isNotEmail(email)) { 33 | console.log(`${email} is not valid.`) 34 | } else { 35 | console.log(`${email} is valid.`) 36 | } 37 | ``` 38 | 39 | ### `isAsciiEmail()` 40 | 41 | An ASCII only version of the validator. 42 | 43 | Since internationalized email addresses are not fully supported by all infrastructure, some systems may want to limit what they consider valid. See [International email](https://en.wikipedia.org/wiki/International_email) and the [Email Address Internationalization](https://en.wikipedia.org/wiki/Email_address#Internationalization) on Wikipedia for more information. 44 | 45 | ```js 46 | const isAsciiEmail = require('sane-email-validation').isAsciiEmail 47 | const email = '...' 48 | 49 | if (isAsciiEmail(email)) { 50 | console.log(`${email} is valid.`) 51 | } else { 52 | console.log(`${email} is not valid.`) 53 | } 54 | ``` 55 | 56 | ### `isNotAsciiEmail()` 57 | 58 | An inverted check is also exposed for the ASCII only version. 59 | 60 | ```js 61 | const isNotAsciiEmail = require('sane-email-validation').isNotAsciiEmail 62 | const email = '...' 63 | 64 | if (isNotAsciiEmail(email)) { 65 | console.log(`${email} is not valid.`) 66 | } else { 67 | console.log(`${email} is valid.`) 68 | } 69 | ``` 70 | 71 | ## License 72 | 73 | Copyright Sane Email Validation contributors. Released under the terms of the MIT license. 74 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const assert = require('assert') 4 | const isAsciiEmail = require('../index').isAsciiEmail 5 | const isEmail = require('../index') 6 | const isNotAsciiEmail = require('../index').isNotAsciiEmail 7 | const isNotEmail = require('../index').isNotEmail 8 | 9 | describe('isEmail', () => { 10 | it('empty', () => { 11 | assert.strictEqual(isEmail(''), false, 'Should not accept empty email.') 12 | }) 13 | 14 | it('invalid', () => { 15 | const longLabel = new Array(65).join('a') 16 | 17 | assert.strictEqual(isEmail('debt'), false, 18 | 'Cannot be local only.') 19 | assert.strictEqual(isEmail('@example.com'), false, 20 | 'Cannot be domain only.') 21 | assert.strictEqual(isEmail('debt@example'), false, 22 | 'Cannot have a domain with only one label.') 23 | assert.strictEqual(isEmail('debt@-example.com'), false, 24 | 'Cannot start domain with a hyphen.') 25 | assert.strictEqual(isEmail('debt@example-.com'), false, 26 | 'Cannot end domain with a hyphen.') 27 | assert.strictEqual(isEmail('debt@example!com'), false, 28 | 'Cannot contain special characters in domain.') 29 | assert.strictEqual(isEmail('debt@' + longLabel + '.com'), false, 30 | 'Cannot contain domain label >63 characters.') 31 | assert.strictEqual(isEmail('debt.@example.com'), false, 32 | 'Cannot end name with dot.') 33 | }) 34 | 35 | it('valid', () => { 36 | const longLabel = new Array(64).join('a') 37 | 38 | assert.strictEqual(isEmail(longLabel + longLabel + '@example.com'), true, 39 | 'Should accept very long local address.') 40 | assert.strictEqual(isEmail('debt@' + longLabel + '.com'), true, 41 | 'Should accept 63 character domain labels.') 42 | assert.strictEqual(isEmail(".!#$%&'*+/=?^_`{|}~-a9@example.com"), true, 43 | 'Should accept certain special characters in local address.') 44 | assert.strictEqual(isEmail('👋@example.com'), true, 45 | 'Should accept unicode in local address') 46 | assert.strictEqual(isEmail('debt@👋.com'), true, 47 | 'Should accept unicode in domain') 48 | assert.strictEqual(isEmail('debt@xn--wp8h.com'), true, 49 | 'Should accept punycoded domains') 50 | assert.strictEqual(isEmail('debt@billing.👋.com'), true, 51 | 'Should accept unicode in every domain label') 52 | }) 53 | }) 54 | 55 | describe('isAsciiEmail', () => { 56 | it('empty', () => { 57 | assert.strictEqual(isEmail(''), false, 'Should not accept empty email.') 58 | }) 59 | 60 | it('invalid', () => { 61 | const longLabel = new Array(65).join('a') 62 | 63 | assert.strictEqual(isAsciiEmail('debt'), false, 64 | 'Cannot be local only.') 65 | assert.strictEqual(isAsciiEmail('@example.com'), false, 66 | 'Cannot be domain only.') 67 | assert.strictEqual(isAsciiEmail('debt@example'), false, 68 | 'Cannot have a domain with only one label.') 69 | assert.strictEqual(isAsciiEmail('debt@-example.com'), false, 70 | 'Cannot start domain with a hyphen.') 71 | assert.strictEqual(isAsciiEmail('debt@example-.com'), false, 72 | 'Cannot end domain with a hyphen.') 73 | assert.strictEqual(isAsciiEmail('debt@example!com'), false, 74 | 'Cannot contain special characters in domain.') 75 | assert.strictEqual(isAsciiEmail('debt@' + longLabel + '.com'), false, 76 | 'Cannot contain domain label >63 characters.') 77 | assert.strictEqual(isAsciiEmail('debt.@example.com'), false, 78 | 'Cannot end name with dot.') 79 | assert.strictEqual(isAsciiEmail('👋@example.com'), false, 80 | 'Should not accept unicode in local address') 81 | assert.strictEqual(isAsciiEmail('debt@👋.com'), false, 82 | 'Should not accept unicode in domain') 83 | assert.strictEqual(isAsciiEmail('debt@xn--wp8h.com'), false, 84 | 'Should not accept punycoded domains') 85 | assert.strictEqual(isAsciiEmail('debt@billing.xn--wp8h.com'), false, 86 | 'Should not accept any punycoded labels') 87 | assert.strictEqual(isAsciiEmail('debt@billing.👋.com'), false, 88 | 'Should not accept unicode in any domain label') 89 | }) 90 | 91 | it('valid', () => { 92 | const longLabel = new Array(64).join('a') 93 | 94 | assert.strictEqual(isAsciiEmail(longLabel + longLabel + '@example.com'), true, 95 | 'Should accept very long local address.') 96 | assert.strictEqual(isAsciiEmail('debt@' + longLabel + '.com'), true, 97 | 'Should accept 63 character domain labels.') 98 | assert.strictEqual(isAsciiEmail(".!#$%&'*+/=?^_`{|}~-a9@example.com"), true, 99 | 'Should accept certain special characters in local address.') 100 | }) 101 | }) 102 | 103 | describe('isNotEmail', () => { 104 | it('empty', () => { 105 | assert.strictEqual(isNotEmail(''), true, 'Should not accept empty email.') 106 | }) 107 | 108 | it('invalid', () => { 109 | const longLabel = new Array(65).join('a') 110 | 111 | assert.strictEqual(isNotEmail('debt'), true, 112 | 'Cannot be local only.') 113 | assert.strictEqual(isNotEmail('@example.com'), true, 114 | 'Cannot be domain only.') 115 | assert.strictEqual(isNotEmail('debt@example'), true, 116 | 'Cannot have a domain with only one label.') 117 | assert.strictEqual(isNotEmail('debt@-example.com'), true, 118 | 'Cannot start domain with a hyphen.') 119 | assert.strictEqual(isNotEmail('debt@example-.com'), true, 120 | 'Cannot end domain with a hyphen.') 121 | assert.strictEqual(isNotEmail('debt@example!com'), true, 122 | 'Cannot contain special characters in domain.') 123 | assert.strictEqual(isNotEmail('debt@' + longLabel + '.com'), true, 124 | 'Cannot contain domain label >63 characters.') 125 | assert.strictEqual(isNotEmail('debt.@example.com'), true, 126 | 'Cannot end name with dot.') 127 | }) 128 | 129 | it('valid', () => { 130 | const longLabel = new Array(64).join('a') 131 | 132 | assert.strictEqual(isNotEmail(longLabel + longLabel + '@example.com'), false, 133 | 'Should accept very long local address.') 134 | assert.strictEqual(isNotEmail('debt@' + longLabel + '.com'), false, 135 | 'Should accept 63 character domain labels.') 136 | assert.strictEqual(isNotEmail(".!#$%&'*+/=?^_`{|}~-a9@example.com"), false, 137 | 'Should accept certain special characters in local address.') 138 | assert.strictEqual(isNotEmail('👋@example.com'), false, 139 | 'Should accept unicode in local address') 140 | assert.strictEqual(isNotEmail('debt@👋.com'), false, 141 | 'Should accept unicode in domain') 142 | assert.strictEqual(isNotEmail('debt@xn--wp8h.com'), false, 143 | 'Should accept punycoded domains') 144 | assert.strictEqual(isNotEmail('debt@billing.👋.com'), false, 145 | 'Should accept unicode in every domain label') 146 | }) 147 | }) 148 | 149 | describe('isNotAsciiEmail', () => { 150 | it('empty', () => { 151 | assert.strictEqual(isNotEmail(''), true, 'Should not accept empty email.') 152 | }) 153 | 154 | it('invalid', () => { 155 | const longLabel = new Array(65).join('a') 156 | 157 | assert.strictEqual(isNotAsciiEmail('debt'), true, 158 | 'Cannot be local only.') 159 | assert.strictEqual(isNotAsciiEmail('@example.com'), true, 160 | 'Cannot be domain only.') 161 | assert.strictEqual(isNotAsciiEmail('debt@example'), true, 162 | 'Cannot have a domain with only one label.') 163 | assert.strictEqual(isNotAsciiEmail('debt@-example.com'), true, 164 | 'Cannot start domain with a hyphen.') 165 | assert.strictEqual(isNotAsciiEmail('debt@example-.com'), true, 166 | 'Cannot end domain with a hyphen.') 167 | assert.strictEqual(isNotAsciiEmail('debt@example!com'), true, 168 | 'Cannot contain special characters in domain.') 169 | assert.strictEqual(isNotAsciiEmail('debt@' + longLabel + '.com'), true, 170 | 'Cannot contain domain label >63 characters.') 171 | assert.strictEqual(isNotAsciiEmail('debt.@example.com'), true, 172 | 'Cannot end name with dot.') 173 | assert.strictEqual(isNotAsciiEmail('👋@example.com'), true, 174 | 'Cannot contain unicode in local address') 175 | assert.strictEqual(isNotAsciiEmail('debt@👋.com'), true, 176 | 'Cannot contain unicode in domain') 177 | assert.strictEqual(isNotAsciiEmail('debt@xn--wp8h.com'), true, 178 | 'Cannot contain punycoded domains') 179 | assert.strictEqual(isNotAsciiEmail('debt@billing.xn--wp8h.com'), true, 180 | 'Cannot contain any punycoded labels') 181 | assert.strictEqual(isNotAsciiEmail('debt@billing.👋.com'), true, 182 | 'Cannot contain unicode in any domain label') 183 | }) 184 | 185 | it('valid', () => { 186 | const longLabel = new Array(64).join('a') 187 | 188 | assert.strictEqual(isNotAsciiEmail(longLabel + longLabel + '@example.com'), false, 189 | 'Should accept very long local address.') 190 | assert.strictEqual(isNotAsciiEmail('debt@' + longLabel + '.com'), false, 191 | 'Should accept 63 character domain labels.') 192 | assert.strictEqual(isNotAsciiEmail(".!#$%&'*+/=?^_`{|}~-a9@example.com"), false, 193 | 'Should accept certain special characters in local address.') 194 | }) 195 | }) 196 | --------------------------------------------------------------------------------