Skip to content

Commit

Permalink
Seems to be working
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertFischer committed Jul 20, 2018
1 parent 8bb3a93 commit dd121aa
Show file tree
Hide file tree
Showing 5 changed files with 1,266 additions and 0 deletions.
130 changes: 130 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{
"env": {
"es6": true,
"node": true,
"browser": true
},
"extends": [ "eslint:recommended", "plugin:react/recommended", "plugin:react-native/all", "plugin:lodash/recommended" ],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react",
"react-native",
"react-filenames",
"lodash",
"dependencies"
],
"rules": {
"indent": [ "error", 2 ],
"linebreak-style": [ "error", "unix" ],
"semi": [ "warn", "always" ],
"no-await-in-loop": [ "error" ],
"no-prototype-builtins": [ "error" ],
"no-template-curly-in-string": [ "warn" ],
"array-callback-return": [ "error" ],
"block-scoped-var": [ "warn" ],
"complexity": [ "warn" ],
"consistent-return": [ "error" ],
"curly": [ "error", "multi-line" ],
"default-case": [ "error" ],
"dot-location": [ "warn", "object" ],
"dot-notation": [ "warn", {"allowKeywords": true} ],
"eqeqeq": [ "error" ],
"no-alert": [ "error" ],
"no-caller": [ "error" ],
"no-div-regex": [ "error" ],
"no-eq-null": [ "error" ],
"no-eval": [ "error" ],
"no-extra-bind": [ "warn" ],
"no-extra-label": [ "warn" ],
"no-floating-decimal": [ "error" ],
"no-implicit-globals": [ "error" ],
"no-implied-eval": [ "error" ],
"no-invalid-this": [ "error" ],
"no-iterator": [ "error" ],
"no-lone-blocks": [ "warn" ],
"no-loop-func": [ "error" ],
"no-magic-numbers": [ "error", { "ignore": [0,1] } ],
"no-new-func": [ "error" ],
"no-new-wrappers": [ "error" ],
"no-proto": [ "error" ],
"no-return-assign": [ "error" ],
"no-return-await": [ "error" ],
"no-self-compare": [ "error" ],
"no-sequences": [ "error" ],
"no-unmodified-loop-condition": [ "warn" ],
"no-unused-expressions": [ "error", { "allowTernary": true, "allowShortCircuit": true } ],
"no-useless-call": [ "error" ],
"no-useless-return": [ "error" ],
"no-void": [ "error" ],
"no-with": [ "error" ],
"radix": [ "error", "always" ],
"require-await": [ "error" ],
"strict": [ "error", "global" ],
"no-label-var": [ "error" ],
"no-shadow": [ "error" ],
"no-shadow-restricted-names": [ "error" ],
"no-undef-init": [ "error" ],
"no-use-before-define": [ "error" ],
"no-bitwise": [ "error" ],
"no-var": [ "error" ],
"prefer-arrow-callback": [ "warn" ],
"prefer-const": [ "error" ],
"prefer-destructuring": [
"error",
{"array": false},
{"enforceForRenamedProperties": false}
],
"prefer-rest-params": [ "warn" ],
"prefer-spread": [ "warn" ],
"prefer-template": [ "warn" ],
"yield-star-spacing": [ "error", "after" ],
"callback-return": [ "error" ],
"handle-callback-err": [ "error" ],
"no-buffer-constructor": [ "error" ],
"no-sync": [ "error", {"allowAtRootLevel": true} ],
"array-bracket-newline": [ "warn", "consistent" ],
"camelcase": [ "warn", { "ignoreDestructuring": true } ],
"comma-dangle": [
"error",
{ "arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "never"
}
],
"function-paren-newline": [ "warn" ],
"implicit-arrow-linebreak": [ "error", "beside" ],
"no-mixed-operators": [ "error" ],
"no-trailing-spaces": [ "warn", { "ignoreComments": true } ],
"no-unneeded-ternary": [ "warn" ],
"prefer-object-spread": [ "error" ],
"arrow-body-style": [ "error", "as-needed" ],
"arrow-parens": [ "error" ],
"generator-star-spacing": [ "error", "after" ],
"no-confusing-arrow": [ "error" ],
"no-duplicate-imports": [ "error", { "includeExports": true } ],
"no-useless-computed-key": [ "warn" ],
"no-useless-rename": [ "error" ],
"no-await-in-loop": [ "error" ],
"no-console": [ "error", { "allow": ["debug","warn","error"] } ],
"lodash/import-scope": [ "off" ],
"lodash/prefer-lodash-method": [ "off" ],
"lodash/prop-shorthand": [ "warn" ],
"lodash/path-style": [ "warn" ],
"template-tag-spacing": [ "warn" ],
"react-native/no-inline-styles": [ "warn" ],
"react/prop-types": [ "warn" ],
"dependencies/case-sensitive": [ "error" ],
"dependencies/no-cycles": [ "warn" ],
"dependencies/no-unresolved": [ "off" ],
"dependencies/require-json-ext": [ "warn" ],
"react-filenames/filename-matches-component": [ "off" ]
}
}
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,39 @@
# react-native-email-deep-validator
Goes beyond validating the characters that make up the email, and instead performs checks on the network to ensure that the e-mail address is valid.

# Synposis

```bash
yarn add react-native-email-validator
```

```javascript
import validateEmail from "react-native-email-validator"

await validateEmail("developer+rnev@beewell.health"); // returns true
await validateEmail("@beewell.health"); // returns false
await validateEmail("developer+rnev@health"); // returns false
```

# Validation Phases

## Step 1: Syntax

* It first ensures that the value is a non-empty string, that the character '`@`' is somewhere in that string, and that it is not the first or last character.
* Then uses [validator/lib/isEmail](https://www.npmjs.com/package/validator) for validating the e-mail address format, which amounts to firing a big ugly regexp against it.

## Step 2: Network Checks

* Calls out to [the `1.1.1.1` DNS over HTTPS server](https://developers.cloudflare.com/1.1.1.1/dns-over-https/) to ensure that there exists at least one MX record for the domain.
* At the same time, calls out to the [Kickbox "disposable email" API](https://open.kickbox.com/v1/disposable/beewell.health) to validate that the e-mail domain is not a disposable email domain.
* It'd be nice to do an SMTP check, but it's unclear how to do that from within React Native without unlinking.

Note that any network check that errors out will be counted as a pass. So if there is no internet, the internet connection is very slow, or the server is down, then it is equivalent to the network check returning valid.

# License

MIT. See the file named `LICENSE` in this directory for details.

# Inspiration

[email-deep-validator](https://github.com/getconversio/email-deep-validator/)
101 changes: 101 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@


const { isNil, isEmpty, isString, merge, not } = require("lodash");
const looksLikeEmail = require("validator/lib/isEmail");
const Promise = require("bluebird");
const URI = require("urijs");

const atSymbol = "@";
const getDnsOverHttpUri = (
(baseUri) => (domainName) => baseUri.clone().setQuery({name: domainName})
)(new URI("https://cloudflare-dns.com/dns-query").setQuery({
'type': 'MX',
'do': true,
'cd': false,
}));
const getBurnerCheckUri = (
(baseUri) => (domainName) => baseUri.clone().filename(domainName)
)(new URI("https://open.kickbox.com/v1/disposable/beewell.health"));

const doFetch = (uri, customOptions={}) => Promise.try(() => fetch(uri, merge({
credentials: 'omit',
mode: 'no-cors',
method: 'GET',
}, customOptions)).tap((response) => {
if(!response.ok) throw new Error("Network error");
}).get("body").call("json"));


module.exports = (addr) => {
const onError = (msg) => (error) => {
console.warn(`Error while ${msg}; returning true by default`, { addr, error});
return true;
};
const logResults = (msg) => (result) => console.debug(
`Results from ${msg} => ${result}`,
{ addr, result }
);

const timeLimitMs = 5000;

const runCheck = (msg, func) => Promise.try(() => func(addr)).timeout(timeLimitMs).catch(onError(msg)).tap(logResults(msg));

const validatingName = "validating email address";
return runCheck(validatingName, () => {

// Basic sanity checks.
if(isNil(addr)) return false;
if(!(isString(addr))) return false;
if(isEmpty(addr.trim())) return false;

// Ensure that '@' is not the first or last char.
if(addr.startsWith(atSymbol)) return false;
if(addr.endsWith(atSymbol)) return false;

// Now split to get the username and domain name
const [username, domainName, ...addrExtra] = addr.split(atSymbol);
if(!(isNil(addrExtra) || isEmpty(addrExtra))) return false;
if(isNil(username) || isNil(domainName)) return false;
if(isEmpty(username.trim()) || isEmpty(domainName.trim())) return false;

// Construct the checks.
const syntaxCheckName = "performing the syntax check";
const syntaxCheck = runCheck( syntaxCheckName, looksLikeEmail );

const mxRecordCheckName = "performing the MX record DNS check";
const mxRecordCheck = runCheck(
mxRecordCheckName,
() => doFetch(
getDnsOverHttpUri(domainName),
{ headers: { accept: 'application/dns-json' } }
).tap((result) => {
if(result.Status !== 0) {
throw new Error(`Bad status code from DNS query: ${result.Status}`);
}
}).get("Answer").then((answer) => {
if(isNil(answer)) {
throw new Error(`No answer returned: ${answer}`);
}
if(isEmpty(answer)) {
return false;
}
return true;
})
);

const burnerName = "performing burner e-mail check";
const burnerCheck = runCheck(
burnerName,
() => doFetch(getBurnerCheckUri(domainName)).get("disposable").then((result) => {
if(isNil(result)) {
throw new Error(`No result returned: ${result}`);
}
return !result;
})
);

const compilingName = "compiling results";
return runCheck(compilingName, () => Promise.filter([syntaxCheck,mxRecordCheck,burnerCheck], not).then((failures) => isNil(failures) || isEmpty(failures)));

});
};
28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "react-native-email-validator",
"version": "1.0.0",
"description": "Goes beyond validating the characters that make up the email, and additionally performs DNS and mailbox checks to ensure that the email address is valid.",
"main": "index.js",
"repository": "git@github.com:BeeWell/react-native-email-deep-validator.git",
"author": "Robert Fischer <robert+github@getbeewell.com>",
"license": "MIT",
"private": false,
"sideEffects": false,
"dependencies": {
"bluebird": "^3.5.1",
"lodash": "^4.17.10",
"urijs": "^1.19.1",
"validator": "^10.4.0"
},
"devDependencies": {
"eslint": "^5.1.0",
"eslint-plugin-dependencies": "^2.4.0",
"eslint-plugin-lodash": "^2.7.0",
"eslint-plugin-react": "^7.10.0",
"eslint-plugin-react-filenames": "^0.0.2",
"eslint-plugin-react-native": "^3.2.1"
},
"scripts": {
"eslint": "eslint --cache --fix ./index.js 2>&1 1>/dev/null || eslint --cache --color --report-unused-disable-directives ./index.js"
}
}
Loading

0 comments on commit dd121aa

Please sign in to comment.