diff --git a/README.md b/README.md index 985cb9ec9..dc25f0c94 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ Sanitizer | Description **blacklist(input, chars)** | remove characters that appear in the blacklist. The characters are used in a RegExp and so you will need to escape some chars, e.g. `blacklist(input, '\\[\\]')`. **escape(input)** | replace `<`, `>`, `&`, `'`, `"`, `` ` ``, `\` and `/` with HTML entities. **ltrim(input [, chars])** | trim characters from the left-side of the input. -**normalizeEmail(email [, options])** | canonicalize an email address. (This doesn't validate that the input is an email, if you want to validate the email use isEmail beforehand).

`options` is an object with the following keys and default values:
+**normalizeEmail(email [, options])** | canonicalize an email address. (This doesn't validate that the input is an email, if you want to validate the email use isEmail beforehand).

`options` is an object with the following keys and default values:
**rtrim(input [, chars])** | trim characters from the right-side of the input. **stripLow(input [, keep_new_lines])** | remove characters with a numerical value < 32 and 127, mostly control characters. If `keep_new_lines` is `true`, newline characters are preserved (`\n` and `\r`, hex `0xA` and `0xD`). Unicode-safe in JavaScript. **toBoolean(input [, strict])** | convert the input string to a boolean. Everything except for `'0'`, `'false'` and `''` returns `true`. In strict mode only `'1'` and `'true'` return `true`. diff --git a/src/lib/normalizeEmail.js b/src/lib/normalizeEmail.js index ceb252f34..6a012239d 100644 --- a/src/lib/normalizeEmail.js +++ b/src/lib/normalizeEmail.js @@ -32,6 +32,8 @@ const default_normalize_email_options = { // The following conversions are specific to Yandex // Lowercases the local part of the Yandex address (known to be case-insensitive) yandex_lowercase: true, + // Removes the subaddress (e.g. "+foo") from the email address + yandex_remove_subaddress: true, // all yandex domains are equal, this explicitly sets the domain to 'yandex.ru' yandex_convert_yandexru: true, @@ -40,6 +42,52 @@ const default_normalize_email_options = { icloud_lowercase: true, // Removes the subaddress (e.g. "+foo") from the email address icloud_remove_subaddress: true, + + // The following conversions are specific to Comcast + // Lowercases the local part of the Comcast address (known to be case-insensitive) + comcast_lowercase: true, + // Removes the subaddress (e.g. "+foo") from the email address + comcast_remove_subaddress: true, + + // The following conversions are specific to FastMail + // Lowercases the local part of the FastMail address (known to be case-insensitive) + fastmail_lowercase: true, + // Removes dots from the local part of the email address, as that's ignored by FastMail + fastmail_remove_dots: true, + // Removes the subaddress (e.g. "+foo") from the email address + fastmail_remove_subaddress: true, + + // The following conversions are specific to GMX + // Lowercases the local part of the GMX address (known to be case-insensitive) + gmx_lowercase: true, + // Removes the subaddress (e.g. "-foo") from the email address + gmx_remove_subaddress: true, + + // The following conversions are specific to Mailfence + // Lowercases the local part of the Mailfence address (known to be case-insensitive) + mailfence_lowercase: true, + // Removes the subaddress (e.g. "+foo") from the email address + mailfence_remove_subaddress: true, + + // The following conversions are specific to Proton + // Lowercases the local part of the Proton address (known to be case-insensitive) + proton_lowercase: true, + // Removes the subaddress (e.g. "+foo") from the email address + proton_remove_subaddress: true, + + // The following conversions are specific to Skiff + // Lowercases the local part of the Skiff address (known to be case-insensitive) + skiff_lowercase: true, + // Removes dots from the local part of the email address, as that's ignored by Skiff + skiff_remove_dots: true, + // Removes the subaddress (e.g. "+foo") from the email address + skiff_remove_subaddress: true, + + // The following conversions are specific to Zoho + // Lowercases the local part of the Zoho address (known to be case-insensitive) + zoho_lowercase: true, + // Removes the subaddress (e.g. "+foo") from the email address + zoho_remove_subaddress: true, }; // List of domains used by iCloud @@ -149,6 +197,8 @@ const yahoo_domains = [ 'yahoo.in', 'yahoo.it', 'ymail.com', + 'cox.net', + 'sbcglobal.net', ]; // List of domains used by yandex.ru @@ -161,6 +211,72 @@ const yandex_domains = [ 'ya.ru', ]; +// List of domains used by Comcast +const comcast_domains = [ + 'comcast.com', + 'comcast.net', +]; + +// List of domains used by FastMail +const fastmail_domains = [ + 'fastmail.com', + 'fastmail.cn', + 'fastmail.co.uk', + 'fastmail.com.au', + 'fastmail.de', + 'fastmail.es', + 'fastmail.fm', + 'fastmail.fr', + 'fastmail.im', + 'fastmail.in', + 'fastmail.jp', + 'fastmail.mx', + 'fastmail.net', + 'fastmail.nl', + 'fastmail.org', + 'fastmail.se', + 'fastmail.to', + 'fastmail.tw', + 'fastmail.uk', + 'fastmail.us', + 'sent.com', +]; + +// List of domains used by GMX +const gmx_domains = [ + 'gmx.at', + 'gmx.ca', + 'gmx.ch', + 'gmx.co.uk', + 'gmx.com', + 'gmx.de', + 'gmx.es', + 'gmx.fr', + 'gmx.net', + 'gmx.us', +]; + +// List of domains used by Mailfence +const mailfence_domains = [ + 'mailfence.com', +]; + +// List of domains used by Proton +const proton_domains = [ + 'proton.me', + 'protonmail.com', +]; + +// List of domains used by Skiff +const skiff_domains = [ + 'skiff.com', +]; + +// List of domains used by Zoho +const zoho_domains = [ + 'zohomail.com', +]; + // replace single dots, but not multiple consecutive dots function dotsReplacer(match) { if (match.length > 1) { @@ -231,10 +347,101 @@ export default function normalizeEmail(email, options) { parts[0] = parts[0].toLowerCase(); } } else if (yandex_domains.indexOf(parts[1]) >= 0) { + // Address is Yandex + if (options.yandex_remove_subaddress) { + parts[0] = parts[0].split('+')[0]; + } + if (!parts[0].length) { + return false; + } if (options.all_lowercase || options.yandex_lowercase) { parts[0] = parts[0].toLowerCase(); } parts[1] = options.yandex_convert_yandexru ? 'yandex.ru' : parts[1]; + } else if (comcast_domains.indexOf(parts[1]) >= 0) { + // Address is Comcast + if (options.comcast_remove_subaddress) { + parts[0] = parts[0].split('+')[0]; + } + if (!parts[0].length) { + return false; + } + if (options.all_lowercase || options.comcast_lowercase) { + parts[0] = parts[0].toLowerCase(); + } + } else if (fastmail_domains.indexOf(parts[1]) >= 0) { + // Address is FastMail + if (options.fastmail_remove_subaddress) { + parts[0] = parts[0].split('+')[0]; + } + if (options.fastmail_remove_dots) { + parts[0] = parts[0].replace(/\.+/g, dotsReplacer); + } + if (!parts[0].length) { + return false; + } + if (options.all_lowercase || options.fastmail_lowercase) { + parts[0] = parts[0].toLowerCase(); + } + } else if (gmx_domains.indexOf(parts[1]) >= 0) { + // Address is GMX (uses - for subaddress like Yahoo) + if (options.gmx_remove_subaddress) { + let components = parts[0].split('-'); + parts[0] = (components.length > 1) ? components.slice(0, -1).join('-') : components[0]; + } + if (!parts[0].length) { + return false; + } + if (options.all_lowercase || options.gmx_lowercase) { + parts[0] = parts[0].toLowerCase(); + } + } else if (mailfence_domains.indexOf(parts[1]) >= 0) { + // Address is Mailfence + if (options.mailfence_remove_subaddress) { + parts[0] = parts[0].split('+')[0]; + } + if (!parts[0].length) { + return false; + } + if (options.all_lowercase || options.mailfence_lowercase) { + parts[0] = parts[0].toLowerCase(); + } + } else if (proton_domains.indexOf(parts[1]) >= 0) { + // Address is Proton + if (options.proton_remove_subaddress) { + parts[0] = parts[0].split('+')[0]; + } + if (!parts[0].length) { + return false; + } + if (options.all_lowercase || options.proton_lowercase) { + parts[0] = parts[0].toLowerCase(); + } + } else if (skiff_domains.indexOf(parts[1]) >= 0) { + // Address is Skiff + if (options.skiff_remove_subaddress) { + parts[0] = parts[0].split('+')[0]; + } + if (options.skiff_remove_dots) { + parts[0] = parts[0].replace(/\.+/g, dotsReplacer); + } + if (!parts[0].length) { + return false; + } + if (options.all_lowercase || options.skiff_lowercase) { + parts[0] = parts[0].toLowerCase(); + } + } else if (zoho_domains.indexOf(parts[1]) >= 0) { + // Address is Zoho + if (options.zoho_remove_subaddress) { + parts[0] = parts[0].split('+')[0]; + } + if (!parts[0].length) { + return false; + } + if (options.all_lowercase || options.zoho_lowercase) { + parts[0] = parts[0].toLowerCase(); + } } else if (options.all_lowercase) { // Any other address parts[0] = parts[0].toLowerCase(); diff --git a/test/sanitizers.test.js b/test/sanitizers.test.js index e36ba48d3..62886a0d3 100644 --- a/test/sanitizers.test.js +++ b/test/sanitizers.test.js @@ -158,16 +158,16 @@ describe('Sanitizers', () => { sanitizer: 'escape', expect: { '': - '<script> alert("xss&fun"); </script>', + '<script> alert("xss&fun"); </script>', "": - '<script> alert('xss&fun'); </script>', + '<script> alert('xss&fun'); </script>', 'Backtick: `': - 'Backtick: `', + 'Backtick: `', 'Backslash: \\': - 'Backslash: \', + 'Backslash: \', }, }); }); @@ -177,16 +177,16 @@ describe('Sanitizers', () => { sanitizer: 'unescape', expect: { '<script> alert("xss&fun"); </script>': - '', + '', '<script> alert('xss&fun'); </script>': - "", + "", 'Backtick: `': - 'Backtick: `', + 'Backtick: `', 'Escaped string: &lt;': - 'Escaped string: <', + 'Escaped string: <', }, }); }); @@ -313,6 +313,7 @@ describe('Sanitizers', () => { '@icloud.com': false, '@outlook.com': false, '@yahoo.com': false, + '@yandex.ru': false, }, }); @@ -510,5 +511,222 @@ describe('Sanitizers', () => { 'test@yandex.by': 'test@yandex.ru', }, }); + + // Testing yandex_remove_subaddress + test({ + sanitizer: 'normalizeEmail', + args: [{ + yandex_remove_subaddress: true, + }], + expect: { + 'test+foo@yandex.ru': 'test@yandex.ru', + 'test+bar@ya.ru': 'test@yandex.ru', + }, + }); + + test({ + sanitizer: 'normalizeEmail', + args: [{ + yandex_remove_subaddress: false, + }], + expect: { + 'test+foo@yandex.ru': 'test+foo@yandex.ru', + 'test+bar@ya.ru': 'test+bar@yandex.ru', + }, + }); + + // Testing new Yahoo domains (cox.net, sbcglobal.net) + test({ + sanitizer: 'normalizeEmail', + expect: { + 'foo-bar@cox.net': 'foo@cox.net', + 'foo-bar@sbcglobal.net': 'foo@sbcglobal.net', + 'FOO@cox.net': 'foo@cox.net', + }, + }); + + // Testing Comcast normalization + test({ + sanitizer: 'normalizeEmail', + expect: { + 'test@comcast.com': 'test@comcast.com', + 'test@comcast.net': 'test@comcast.net', + 'test+foo@comcast.com': 'test@comcast.com', + 'TEST@comcast.COM': 'test@comcast.com', + '@comcast.com': false, + }, + }); + + test({ + sanitizer: 'normalizeEmail', + args: [{ + all_lowercase: false, + comcast_remove_subaddress: false, + comcast_lowercase: false, + }], + expect: { + 'test+foo@comcast.com': 'test+foo@comcast.com', + 'TEST@comcast.com': 'TEST@comcast.com', + }, + }); + + // Testing FastMail normalization + test({ + sanitizer: 'normalizeEmail', + expect: { + 'test@fastmail.com': 'test@fastmail.com', + 'test@fastmail.fm': 'test@fastmail.fm', + 'test@sent.com': 'test@sent.com', + 'test+foo@fastmail.com': 'test@fastmail.com', + 'some.name@fastmail.com': 'somename@fastmail.com', + 'some.name+foo@fastmail.com': 'somename@fastmail.com', + 'TEST@fastmail.COM': 'test@fastmail.com', + '@fastmail.com': false, + }, + }); + + test({ + sanitizer: 'normalizeEmail', + args: [{ + all_lowercase: false, + fastmail_remove_subaddress: false, + fastmail_remove_dots: false, + fastmail_lowercase: false, + }], + expect: { + 'test+foo@fastmail.com': 'test+foo@fastmail.com', + 'some.name@fastmail.com': 'some.name@fastmail.com', + 'TEST@fastmail.com': 'TEST@fastmail.com', + }, + }); + + // Testing GMX normalization (uses - for subaddress like Yahoo) + test({ + sanitizer: 'normalizeEmail', + expect: { + 'test@gmx.com': 'test@gmx.com', + 'test@gmx.de': 'test@gmx.de', + 'test@gmx.net': 'test@gmx.net', + 'test-foo@gmx.com': 'test@gmx.com', + 'test-foo-bar@gmx.com': 'test-foo@gmx.com', + 'TEST@gmx.COM': 'test@gmx.com', + '@gmx.com': false, + }, + }); + + test({ + sanitizer: 'normalizeEmail', + args: [{ + all_lowercase: false, + gmx_remove_subaddress: false, + gmx_lowercase: false, + }], + expect: { + 'test-foo@gmx.com': 'test-foo@gmx.com', + 'TEST@gmx.com': 'TEST@gmx.com', + }, + }); + + // Testing Mailfence normalization + test({ + sanitizer: 'normalizeEmail', + expect: { + 'test@mailfence.com': 'test@mailfence.com', + 'test+foo@mailfence.com': 'test@mailfence.com', + 'TEST@mailfence.COM': 'test@mailfence.com', + '@mailfence.com': false, + }, + }); + + test({ + sanitizer: 'normalizeEmail', + args: [{ + all_lowercase: false, + mailfence_remove_subaddress: false, + mailfence_lowercase: false, + }], + expect: { + 'test+foo@mailfence.com': 'test+foo@mailfence.com', + 'TEST@mailfence.com': 'TEST@mailfence.com', + }, + }); + + // Testing Proton normalization + test({ + sanitizer: 'normalizeEmail', + expect: { + 'test@proton.me': 'test@proton.me', + 'test@protonmail.com': 'test@protonmail.com', + 'test+foo@proton.me': 'test@proton.me', + 'test+foo@protonmail.com': 'test@protonmail.com', + 'TEST@proton.ME': 'test@proton.me', + '@proton.me': false, + }, + }); + + test({ + sanitizer: 'normalizeEmail', + args: [{ + all_lowercase: false, + proton_remove_subaddress: false, + proton_lowercase: false, + }], + expect: { + 'test+foo@proton.me': 'test+foo@proton.me', + 'TEST@proton.me': 'TEST@proton.me', + }, + }); + + // Testing Skiff normalization + test({ + sanitizer: 'normalizeEmail', + expect: { + 'test@skiff.com': 'test@skiff.com', + 'test+foo@skiff.com': 'test@skiff.com', + 'some.name@skiff.com': 'somename@skiff.com', + 'some.name+foo@skiff.com': 'somename@skiff.com', + 'TEST@skiff.COM': 'test@skiff.com', + '@skiff.com': false, + }, + }); + + test({ + sanitizer: 'normalizeEmail', + args: [{ + all_lowercase: false, + skiff_remove_subaddress: false, + skiff_remove_dots: false, + skiff_lowercase: false, + }], + expect: { + 'test+foo@skiff.com': 'test+foo@skiff.com', + 'some.name@skiff.com': 'some.name@skiff.com', + 'TEST@skiff.com': 'TEST@skiff.com', + }, + }); + + // Testing Zoho normalization + test({ + sanitizer: 'normalizeEmail', + expect: { + 'test@zohomail.com': 'test@zohomail.com', + 'test+foo@zohomail.com': 'test@zohomail.com', + 'TEST@zohomail.COM': 'test@zohomail.com', + '@zohomail.com': false, + }, + }); + + test({ + sanitizer: 'normalizeEmail', + args: [{ + all_lowercase: false, + zoho_remove_subaddress: false, + zoho_lowercase: false, + }], + expect: { + 'test+foo@zohomail.com': 'test+foo@zohomail.com', + 'TEST@zohomail.com': 'TEST@zohomail.com', + }, + }); }); });