├── .commitlintrc.js ├── .dist.babelrc ├── .dist.eslintrc ├── .editorconfig ├── .eslintignore ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .huskyrc ├── .lib.babelrc ├── .lib.eslintrc ├── .lintstagedrc.js ├── .npmrc ├── .nycrc ├── .prettierrc.js ├── .remarkignore ├── .remarkrc.js ├── .xo-config.js ├── LICENSE ├── README.md ├── package.json ├── src └── index.js └── test ├── browser.js └── test.js /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | }; 4 | -------------------------------------------------------------------------------- /.dist.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { 4 | "targets": { 5 | "browsers": [ "defaults, not ie 11" ] 6 | } 7 | }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.dist.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended"], 3 | "parser": "@babel/eslint-parser", 4 | "parserOptions": { 5 | "requireConfigFile": false, 6 | "babelOptions": { 7 | "babelrc": false, 8 | "configFile": false, 9 | "presets": ["@babel/preset-env"] 10 | } 11 | }, 12 | "env": { 13 | "node": false, 14 | "browser": true, 15 | "amd": true, 16 | "es6": true, 17 | "commonjs": true 18 | }, 19 | "plugins": ["compat"], 20 | "rules": { 21 | "no-unused-vars": "off" 22 | }, 23 | "globals": { 24 | }, 25 | "settings": { 26 | "polyfills": [ 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.*.js 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | node_version: 13 | - 16 14 | - 18 15 | name: Node ${{ matrix.node_version }} on ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Setup node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node_version }} 22 | - name: Install dependencies 23 | run: npm install 24 | - name: Run tests 25 | run: npm run test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | .idea 4 | node_modules 5 | coverage 6 | .nyc_output 7 | locales/ 8 | package-lock.json 9 | yarn.lock 10 | 11 | Thumbs.db 12 | tmp/ 13 | temp/ 14 | *.lcov 15 | .env 16 | lib 17 | dist 18 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged && npm test 5 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lib.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { 4 | "targets": { 5 | "node": "14", 6 | "browsers": [ "defaults, not ie 11" ] 7 | } 8 | }] 9 | ], 10 | "sourceMaps": "both" 11 | } 12 | -------------------------------------------------------------------------------- /.lib.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:node/recommended"], 3 | "env": { "browser": true" }, 4 | "plugins": ["compat"], 5 | "rules": { 6 | }, 7 | "settings": { 8 | "polyfills": [] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.md": filenames => filenames.map(filename => `remark ${filename} -qfo`), 3 | 'package.json': 'fixpack', 4 | '*.js': 'xo --fix' 5 | }; 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".js" 4 | ], 5 | "report-dir": "./coverage", 6 | "temp-dir": "./.nyc_output", 7 | "reporter": ["lcov", "html", "text"] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | bracketSpacing: true, 4 | trailingComma: 'none' 5 | }; 6 | -------------------------------------------------------------------------------- /.remarkignore: -------------------------------------------------------------------------------- 1 | test/snapshots/**/*.md 2 | -------------------------------------------------------------------------------- /.remarkrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['preset-github'] 3 | }; 4 | -------------------------------------------------------------------------------- /.xo-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prettier: true, 3 | space: true, 4 | extends: ['xo-lass'] 5 | }; 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Forward Email LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # email-regex-safe 2 | 3 | [![build status](https://github.com/spamscanner/email-regex-safe/actions/workflows/ci.yml/badge.svg)](https://github.com/spamscanner/email-regex-safe/actions/workflows/ci.yml) 4 | [![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 5 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 6 | [![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org) 7 | [![license](https://img.shields.io/github/license/spamscanner/email-regex-safe.svg)](LICENSE) 8 | [![npm downloads](https://img.shields.io/npm/dt/email-regex-safe.svg)](https://npm.im/email-regex-safe) 9 | 10 | > Regular expression matching for email addresses. Maintained, configurable, more accurate, and browser-friendly alternative to [email-regex][]. Works in Node v14+ and browsers. **Maintained for [Spam Scanner][spam-scanner] and [Forward Email][forward-email]**. 11 | 12 | 13 | ## Table of Contents 14 | 15 | * [Foreword](#foreword) 16 | * [Install](#install) 17 | * [Usage](#usage) 18 | * [Node](#node) 19 | * [Browser](#browser) 20 | * [Options](#options) 21 | * [How to validate an email address](#how-to-validate-an-email-address) 22 | * [Limitations](#limitations) 23 | * [Contributors](#contributors) 24 | * [License](#license) 25 | 26 | 27 | ## Foreword 28 | 29 | Previously we were using [email-regex][] through our work on [Spam Scanner][spam-scanner] and [Forward Email][forward-email]. However this package has [too many issues](https://github.com/sindresorhus/email-regex/issues/9) and [false positives](https://github.com/sindresorhus/email-regex/issues/2). 30 | 31 | This package should hopefully more closely resemble real-world intended usage of an email regular expression, and also let you configure several [Options](#options). Please check out [Forward Email][forward-email] if this package helped you, and explore our source code on GitHub which shows how we use this package. 32 | 33 | **It will not perform strict email validation, but instead hints the complete matches resembling an email address. We recommend to use [validator.isEmail][validator-email] for validation (e.g. `validator.isEmail(match)`).** 34 | 35 | 36 | ## Install 37 | 38 | **NOTE:** The default behavior of this package will attempt to load [re2](https://github.com/uhop/node-re2) (it is an optional peer dependency used to prevent regular expression denial of service attacks and more). If you wish to use this behavior, you must have `re2` installed via `npm install re2` – otherwise it will fallback to using normal `RegExp` instances. As of v4.0.0 we added an option if you wish to force this package to not even attempt to load `re2` (e.g. it's in your `node_modules` [but you don't want to use it](https://github.com/spamscanner/url-regex-safe/issues/28)) – simply pass `re2: false` as an option. 39 | 40 | [npm][]: 41 | 42 | ```sh 43 | npm install email-regex-safe 44 | ``` 45 | 46 | 47 | ## Usage 48 | 49 | ### Node 50 | 51 | ```js 52 | const emailRegexSafe = require('email-regex-safe'); 53 | 54 | const str = 'some long string with foo@bar.com in it'; 55 | const matches = str.match(emailRegexSafe()); 56 | 57 | for (const match of matches) { 58 | console.log('match', match); 59 | } 60 | 61 | console.log(emailRegexSafe({ exact: true }).test('hello@example.com')); 62 | ``` 63 | 64 | ### Browser 65 | 66 | Since [RE2][] is not made for the browser, it will not be used. If there were to be any regex vulnerabilities, they would only crash the user's browser tab, and not your server (as they would on the Node.js side without the use of [RE2][]). 67 | 68 | #### VanillaJS 69 | 70 | This is the solution for you if you're just using ` 74 | 86 | ``` 87 | 88 | #### Bundler 89 | 90 | Assuming you are using [browserify][], [webpack][], [rollup][], or another bundler, you can simply follow [Node](#node) usage above. 91 | 92 | 93 | ## Options 94 | 95 | | Property | Type | Default Value | Description | 96 | | -------------- | ------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 97 | | `re2` | Boolean | `true` | Attempt to load `re2` to use instead of `RegExp` for creating new regular expression instances. If you pass `re2: false`, then `re2` will not even be attempted to be loaded. | 98 | | `exact` | Boolean | `false` | Only match an exact String. Useful with `regex.test(str)` to check if a String is an email address. We set this to `false` by default as the most common use case for a RegExp parser is to parse out emails, as opposed to check strict validity; we feel this closely more resembles real-world intended usage of this package. | 99 | | `strict` | Boolean | `false` | If `true`, then it will allow any TLD as long as it is a minimum of 2 valid characters. If it is `false`, then it will match the TLD against the list of valid TLD's using [tlds](https://github.com/stephenmathieson/node-tlds#readme). | 100 | | `gmail` | Boolean | `true` | Whether or not to abide by Gmail's rules for email usernames (see Gmail's [Create a username article](https://support.google.com/mail/answer/9211434) for more insight). Note that since [RE2][] does not support negative lookahead nor negative lookbehind, we are leaving it up to you to filter out a select few invalid matches while using `gmail: true`. Invalid matches would be those that end with a "." (period) or "+" (plus symbol), or have two or more consecutive ".." periods in a row anywhere in the username portion. We recommend to use `str.matches(emailSafeRegex())` to get an Array of all matches, and then filter those that pass [validator.isEmail][validator-email] after having end period(s) and/or plus symbol(s) stripped from them, as well as filtering out matches with repeated periods. | 101 | | `utf8` | Boolean | `true` | Whether or not to allow UTF-8 characters for email usernames. This Boolean is only applicable if `gmail` option is set to `false`. | 102 | | `localhost` | Boolean | `true` | Allows localhost in the URL hostname portion. See the [test/test.js](test/test.js) for more insight into the localhost test and how it will return a value which may be unwanted. A pull request would be considered to resolve the "pic.jp" vs. "pic.jpg" issue. | 103 | | `ipv4` | Boolean | `true` | Match against IPv4 URL's. | 104 | | `ipv6` | Boolean | `false` | Match against IPv6 URL's. This is set to `false` by default, since IPv6 is not really supported anywhere for email addresses, and it's not even included in [validator.isEmail][validator-email]'s logic. | 105 | | `tlds` | Array | [tlds](https://github.com/stephenmathieson/node-tlds#readme) | Match against a specific list of tlds, or the default list provided by [tlds](https://github.com/stephenmathieson/node-tlds#readme). | 106 | | `returnString` | Boolean | `false` | Return the RegExp as a String instead of a `RegExp` (useful for custom logic, such as we did with [Spam Scanner][spam-scanner]). | 107 | 108 | 109 | ## How to validate an email address 110 | 111 | If you would like to validate email addresses found, then you should use the [validator.isEmail][validator-email] method. This will further enforce the email RFC specification limitations of 64 characters for the username/local part of the email address, 254 for the domain/hostname portion, and 255 in total; including the "@" (at symbol). 112 | 113 | 114 | ## Limitations 115 | 116 | **This limitation only applies if you are using `re2`**: Since we cannot use regular expression's "negative lookbehinds" functionality (due to [RE2][] limitations), we could not merge the logic from this [pull request](https://github.com/kevva/url-regex/pull/67/commits/6c31d81c35c3bb72c413c6e4af92a37b2689ead2). This would have allowed us to make it so `example.jpeg` would match only if it was `example.jp`, however if you pass `example.jpeg` right now it will extract `example.jp` from it (since `.jp` is a TLD). An alternative solution may exist, and we welcome community contributions regarding this issue. 117 | 118 | 119 | ## Contributors 120 | 121 | | Name | Website | 122 | | ----------------- | -------------------------- | 123 | | **Forward Email** | | 124 | 125 | 126 | ## License 127 | 128 | [MIT](LICENSE) © [Forward Email](https://forwardemail.net) 129 | 130 | 131 | ## 132 | 133 | [npm]: https://www.npmjs.com/ 134 | 135 | [re2]: https://github.com/uhop/node-re2 136 | 137 | [browserify]: https://github.com/browserify/browserify 138 | 139 | [webpack]: https://github.com/webpack/webpack 140 | 141 | [rollup]: https://github.com/rollup/rollup 142 | 143 | [email-regex]: https://github.com/sindresorhus/email-regex 144 | 145 | [spam-scanner]: https://spamscanner.net 146 | 147 | [forward-email]: https://forwardemail.net 148 | 149 | [validator-email]: https://github.com/validatorjs/validator.js/blob/master/src/lib/isEmail.js 150 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "email-regex-safe", 3 | "description": "Regular expression matching for email addresses. Maintained, configurable, more accurate, and browser-friendly alternative to email-regex. Works in Node v14+ and browsers. Made for Spam Scanner and Forward Email.", 4 | "version": "4.0.0", 5 | "author": "Forward Email (https://forwardemail.net)", 6 | "browser": { 7 | "re2": false 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/spamscanner/email-regex-safe/issues" 11 | }, 12 | "contributors": [ 13 | "Forward Email (https://forwardemail.net)" 14 | ], 15 | "dependencies": { 16 | "ip-regex": "4", 17 | "tlds": "^1.242.0" 18 | }, 19 | "devDependencies": { 20 | "@babel/cli": "^7.22.10", 21 | "@babel/core": "^7.22.10", 22 | "@babel/eslint-parser": "^7.22.10", 23 | "@babel/preset-env": "^7.22.10", 24 | "@commitlint/cli": "^17.7.1", 25 | "@commitlint/config-conventional": "^17.7.0", 26 | "ava": "^4.3.0", 27 | "babelify": "^10.0.0", 28 | "browserify": "^17.0.0", 29 | "cross-env": "^7.0.3", 30 | "eslint": "^8.47.0", 31 | "eslint-config-xo-lass": "^2.0.1", 32 | "eslint-plugin-compat": "^4.1.4", 33 | "eslint-plugin-node": "^11.1.0", 34 | "fixpack": "^4.0.0", 35 | "husky": "^8.0.3", 36 | "jsdom": "15", 37 | "lint-staged": "^14.0.0", 38 | "nyc": "^15.1.0", 39 | "re2": "^1.20.1", 40 | "remark-cli": "^11.0.0", 41 | "remark-preset-github": "^4.0.4", 42 | "rimraf": "^5.0.1", 43 | "tinyify": "^3.1.0", 44 | "xo": "^0.56.0" 45 | }, 46 | "engines": { 47 | "node": ">=14" 48 | }, 49 | "files": [ 50 | "lib", 51 | "dist" 52 | ], 53 | "homepage": "https://github.com/spamscanner/email-regex-safe", 54 | "jsdelivr": "dist/email-regex-safe.min.js", 55 | "keywords": [ 56 | "2020", 57 | "7661", 58 | "CVE-2020-7661", 59 | "addr", 60 | "address", 61 | "business", 62 | "checker", 63 | "checking", 64 | "checkr", 65 | "cve", 66 | "detect", 67 | "email", 68 | "emails", 69 | "expr", 70 | "expresion", 71 | "expression", 72 | "expression", 73 | "from", 74 | "gapps", 75 | "get", 76 | "gmail", 77 | "google", 78 | "googlemail", 79 | "html", 80 | "mail", 81 | "mails", 82 | "maintained", 83 | "parse", 84 | "parser", 85 | "parsing", 86 | "regex", 87 | "regexer", 88 | "regexer", 89 | "regexes", 90 | "regexing", 91 | "regexp", 92 | "regular", 93 | "safe", 94 | "scan", 95 | "sniff", 96 | "str", 97 | "string", 98 | "syntax", 99 | "text", 100 | "url", 101 | "urls", 102 | "username", 103 | "validation", 104 | "validator" 105 | ], 106 | "license": "MIT", 107 | "main": "lib/index.js", 108 | "peerDependencies": { 109 | "re2": "^1.20.1" 110 | }, 111 | "peerDependenciesMeta": { 112 | "re2": { 113 | "optional": true 114 | } 115 | }, 116 | "repository": { 117 | "type": "git", 118 | "url": "https://github.com/spamscanner/email-regex-safe" 119 | }, 120 | "scripts": { 121 | "browserify": "browserify src/index.js -o dist/email-regex-safe.js -s emailRegexSafe -g [ babelify --configFile ./.dist.babelrc ]", 122 | "build": "npm run build:clean && npm run build:lib && npm run build:dist", 123 | "build:clean": "rimraf lib dist", 124 | "build:dist": "npm run browserify && npm run minify", 125 | "build:lib": "babel --config-file ./.lib.babelrc src --out-dir lib", 126 | "lint": "npm run lint:js && npm run lint:md && npm run lint:pkg && npm run lint:lib && npm run lint:dist", 127 | "lint:dist": "eslint --no-inline-config -c .dist.eslintrc dist", 128 | "lint:js": "xo --fix", 129 | "lint:lib": "eslint -c .lib.eslintrc lib", 130 | "lint:md": "remark . -qfo", 131 | "lint:pkg": "fixpack", 132 | "minify": "cross-env NODE_ENV=production browserify src/index.js -o dist/email-regex-safe.min.js -s emailRegexSafe -g [ babelify --configFile ./.dist.babelrc ] -p tinyify", 133 | "prepare": "husky install", 134 | "pretest": "npm run build && npm run lint", 135 | "test": "cross-env NODE_ENV=test nyc ava" 136 | }, 137 | "unpkg": "dist/email-regex-safe.min.js" 138 | } 139 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const ipRegex = require('ip-regex'); 2 | const tlds = require('tlds'); 3 | 4 | const ipv4 = ipRegex.v4().source; 5 | const ipv6 = ipRegex.v6().source; 6 | const host = '(?:(?:[a-z\\u00a1-\\uffff0-9][-_]*)*[a-z\\u00a1-\\uffff0-9]+)'; 7 | const domain = '(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*'; 8 | const strictTld = '(?:[a-z\\u00a1-\\uffff]{2,})'; 9 | const defaultTlds = `(?:${tlds.sort((a, b) => b.length - a.length).join('|')})`; 10 | 11 | let RE2; 12 | let hasRE2; 13 | 14 | module.exports = (options) => { 15 | options = { 16 | // 17 | // attempt to use re2, if set to false will use RegExp 18 | // (we did this approach because we don't want to load in-memory re2 if users don't want it) 19 | // 20 | // 21 | re2: true, 22 | exact: false, 23 | strict: false, 24 | gmail: true, 25 | utf8: true, 26 | localhost: true, 27 | ipv4: true, 28 | ipv6: false, 29 | returnString: false, 30 | ...options 31 | }; 32 | 33 | /* istanbul ignore next */ 34 | const SafeRegExp = 35 | options.re2 && hasRE2 !== false 36 | ? (() => { 37 | if (typeof RE2 === 'function') return RE2; 38 | try { 39 | RE2 = require('re2'); 40 | return typeof RE2 === 'function' ? RE2 : RegExp; 41 | } catch { 42 | hasRE2 = false; 43 | return RegExp; 44 | } 45 | })() 46 | : RegExp; 47 | 48 | // Add ability to pass custom list of tlds 49 | // 50 | const tld = `(?:\\.${ 51 | options.strict 52 | ? strictTld 53 | : options.tlds 54 | ? `(?:${options.tlds.sort((a, b) => b.length - a.length).join('|')})` 55 | : defaultTlds 56 | })`; 57 | 58 | // 59 | const emailUserPart = options.gmail 60 | ? // https://support.google.com/mail/answer/9211434?hl=en#:~:text=Usernames%20can%20contain%20letters%20(a%2Dz,in%20a%20row. 61 | // cannot contain: &, =, _, ', -, +, comma, brackets, or more than one period in a row 62 | // note that we are parsing for emails, not enforcing username match, so we allow + 63 | '[^\\W_](?:[\\w\\.\\+]+)' // NOTE: we don't end with `[^\\W]` here since Gmail doesn't do this in webmail 64 | : options.utf8 65 | ? "[^\\W_](?:[a-z\\d!#\\$%&'\\.\\*\\+\\-\\/=\\?\\^_`{\\|}~\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]+)" 66 | : "[^\\W_](?:[a-z\\d!#\\$%&'\\.\\*\\+\\-\\/=\\?\\^_`{\\|}~]+)"; 67 | 68 | let regex = `(?:${emailUserPart}@(?:`; 69 | if (options.localhost) regex += 'localhost|'; 70 | if (options.ipv4) regex += `${ipv4}|`; 71 | if (options.ipv6) regex += `${ipv6}|`; 72 | regex += `${host}${domain}${tld}))`; 73 | 74 | // Add option to return the regex string instead of a RegExp 75 | if (options.returnString) return regex; 76 | 77 | return options.exact 78 | ? new SafeRegExp(`(?:^${regex}$)`, 'i') 79 | : new SafeRegExp(regex, 'ig'); 80 | }; 81 | -------------------------------------------------------------------------------- /test/browser.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | const { readFileSync } = require('node:fs'); 3 | const { Script } = require('node:vm'); 4 | const test = require('ava'); 5 | const { JSDOM, VirtualConsole } = require('jsdom'); 6 | 7 | const virtualConsole = new VirtualConsole(); 8 | virtualConsole.sendTo(console); 9 | 10 | const script = new Script( 11 | readFileSync(path.join(__dirname, '..', 'dist', 'email-regex-safe.min.js')) 12 | ); 13 | 14 | const dom = new JSDOM(``, { 15 | url: 'http://localhost:3000/', 16 | referrer: 'http://localhost:3000/', 17 | contentType: 'text/html', 18 | includeNodeLocations: true, 19 | resources: 'usable', 20 | runScripts: 'dangerously', 21 | virtualConsole 22 | }); 23 | 24 | dom.runVMScript(script); 25 | 26 | test('should work in the browser', (t) => { 27 | t.true(typeof dom.window.emailRegexSafe === 'function'); 28 | t.true(dom.window.emailRegexSafe({ exact: true }).test('hello@example.com')); 29 | t.deepEqual( 30 | 'some long string with foo@bar.com in it'.match( 31 | dom.window.emailRegexSafe() 32 | ), 33 | ['foo@bar.com'] 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const emailRegexSafe = require('..'); 3 | 4 | const string = ` 5 | bob.foo-bar@test.com 6 | __boop@beep.com 7 | foo@foo.com 8 | foo@f.com 9 | foo@.com 10 | some@sub.domain.jpg.co.uk.com.jpeg 11 | _._boop@beep.com 12 | bepp.test@boop.com 13 | beep....foo@foo.com 14 | beep..@foo.com 15 | beep@bar.com. 16 | beep.boop.boop.@foo.com 17 | beep@boop.com .@foo.com 18 | foo@_ $foobar@gmail.com 19 | +foo@gmail.com 20 | +$foo@gmail.com 21 | +@test.com 22 | ++@test.com+@testtest.com 23 | url:www.example.com reserved.[subscribe.example.com/subscribe.aspx?foo=zaaaa@example.io&beep=foo124123@example.nl 24 | ##rfc822;beep@test.co.uk 25 | /images/some_logo@2x.jp 26 | /images/foobar@2x.jpeg ----------------------------------------[beep.boop.net/foo.cfm?email=beep@example.ai\nwww.foo-beep.es was invalid 27 | cid:image001.png@01bazz23.mx1e6980]www.facebook.com/example[cid:image002.png@03j570cf.ee1e6980]twitter.com/foobar[cid:image000.png@03j570cfzaaaazz.ee1e6980]http://www.linkedin.com/company/beep?trk=company_logo[cid:image005.png@03j570cf.es 28 | foo@bar example.@gmail.com 29 | foo+test@gmail.com 30 | f=nr@context",c=e("gos") 'text@example.com, some text' 31 | fazboop beep baz@boop.com 32 | foo@fe.com admin@2606:4700:4700::1111 33 | fe@fe az@as test@1.2.3.4 foo@com.jpeg 34 | foo@com.jpeg`; 35 | 36 | test('gmail matches', (t) => { 37 | const match = string.match(emailRegexSafe()); 38 | t.log(match); 39 | t.deepEqual(match, [ 40 | 'bar@test.com', 41 | 'boop@beep.com', 42 | 'foo@foo.com', 43 | 'foo@f.com', 44 | 'some@sub.domain.jpg.co.uk.com.jp', 45 | 'boop@beep.com', 46 | 'bepp.test@boop.com', 47 | 'beep....foo@foo.com', 48 | 'beep..@foo.com', 49 | 'beep@bar.com', 50 | 'beep.boop.boop.@foo.com', 51 | 'beep@boop.com', 52 | 'foobar@gmail.com', 53 | 'foo@gmail.com', 54 | 'foo@gmail.com', 55 | 'test.com+@testtest.com', 56 | 'zaaaa@example.io', 57 | 'foo124123@example.nl', 58 | 'beep@test.co.uk', 59 | 'some_logo@2x.jp', 60 | 'foobar@2x.jp', 61 | 'beep@example.ai', 62 | 'image001.png@01bazz23.mx', 63 | 'image002.png@03j570cf.ee', 64 | 'image000.png@03j570cfzaaaazz.ee', 65 | 'image005.png@03j570cf.es', 66 | 'example.@gmail.com', 67 | 'foo+test@gmail.com', 68 | 'text@example.com', 69 | 'foo@bar.com', 70 | 'baz@boop.com', 71 | 'foo@fe.com', 72 | 'test@1.2.3.4', 73 | 'foo@com.jp', 74 | 'foo@com.jp' 75 | ]); 76 | }); 77 | 78 | test('non-gmail matches', (t) => { 79 | const match = string.match(emailRegexSafe({ gmail: false })); 80 | t.log(match); 81 | t.deepEqual(match, [ 82 | 'bob.foo-bar@test.com', 83 | 'boop@beep.com', 84 | 'foo@foo.com', 85 | 'foo@f.com', 86 | 'some@sub.domain.jpg.co.uk.com.jp', 87 | 'boop@beep.com', 88 | 'bepp.test@boop.com', 89 | 'beep....foo@foo.com', 90 | 'beep..@foo.com', 91 | 'beep@bar.com', 92 | 'beep.boop.boop.@foo.com', 93 | 'beep@boop.com', 94 | 'foobar@gmail.com', 95 | 'foo@gmail.com', 96 | 'foo@gmail.com', 97 | 'test.com+@testtest.com', 98 | 'subscribe.example.com/subscribe.aspx?foo=zaaaa@example.io', 99 | 'beep=foo124123@example.nl', 100 | 'beep@test.co.uk', 101 | 'images/some_logo@2x.jp', 102 | 'images/foobar@2x.jp', 103 | 'beep.boop.net/foo.cfm?email=beep@example.ai', 104 | 'image001.png@01bazz23.mx', 105 | 'image002.png@03j570cf.ee', 106 | 'image000.png@03j570cfzaaaazz.ee', 107 | 'image005.png@03j570cf.es', 108 | 'example.@gmail.com', 109 | 'foo+test@gmail.com', 110 | 'text@example.com', 111 | 'foo@bar.com', 112 | 'baz@boop.com', 113 | 'foo@fe.com', 114 | 'test@1.2.3.4', 115 | 'foo@com.jp', 116 | 'foo@com.jp' 117 | ]); 118 | }); 119 | --------------------------------------------------------------------------------