mailcheck.js
325 lines
| 1 | /* globals define, module, jQuery */ |
| 2 | |
| 3 | /* |
| 4 | * Mailcheck https://github.com/mailcheck/mailcheck |
| 5 | * Author |
| 6 | * Derrick Ko (@derrickko) |
| 7 | * |
| 8 | * Released under the MIT License. |
| 9 | * |
| 10 | * v 1.1.2 |
| 11 | */ |
| 12 | |
| 13 | var Mailcheck = { |
| 14 | domainThreshold: 2, |
| 15 | secondLevelThreshold: 2, |
| 16 | topLevelThreshold: 2, |
| 17 | |
| 18 | defaultDomains: ['msn.com', 'bellsouth.net', |
| 19 | 'telus.net', 'comcast.net', 'optusnet.com.au', |
| 20 | 'earthlink.net', 'qq.com', 'sky.com', 'icloud.com', |
| 21 | 'mac.com', 'sympatico.ca', 'googlemail.com', |
| 22 | 'att.net', 'xtra.co.nz', 'web.de', |
| 23 | 'cox.net', 'gmail.com', 'ymail.com', |
| 24 | 'aim.com', 'rogers.com', 'verizon.net', |
| 25 | 'rocketmail.com', 'google.com', 'optonline.net', |
| 26 | 'sbcglobal.net', 'aol.com', 'me.com', 'btinternet.com', |
| 27 | 'charter.net', 'shaw.ca'], |
| 28 | |
| 29 | defaultSecondLevelDomains: ["yahoo", "hotmail", "mail", "live", "outlook", "gmx"], |
| 30 | |
| 31 | defaultTopLevelDomains: ["com", "edu.au", "com.au", "com.tw", "ca", "co.nz", "co.uk", "de", |
| 32 | "fr", "it", "ru", "net", "org", "edu", "gov", "jp", "nl", "kr", "se", "eu", |
| 33 | "ie", "co.il", "us", "at", "be", "dk", "hk", "es", "gr", "ch", "no", "cz", |
| 34 | "in", "net", "net.au", "info", "biz", "mil", "co.jp", "sg", "hu", "uk"], |
| 35 | |
| 36 | run: function(opts) { |
| 37 | opts.domains = opts.domains || Mailcheck.defaultDomains; |
| 38 | opts.secondLevelDomains = opts.secondLevelDomains || Mailcheck.defaultSecondLevelDomains; |
| 39 | opts.topLevelDomains = opts.topLevelDomains || Mailcheck.defaultTopLevelDomains; |
| 40 | opts.distanceFunction = opts.distanceFunction || Mailcheck.sift4Distance; |
| 41 | |
| 42 | var defaultCallback = function(result){ return result; }; |
| 43 | var suggestedCallback = opts.suggested || defaultCallback; |
| 44 | var emptyCallback = opts.empty || defaultCallback; |
| 45 | |
| 46 | var result = Mailcheck.suggest(Mailcheck.encodeEmail(opts.email), opts.domains, opts.secondLevelDomains, opts.topLevelDomains, opts.distanceFunction); |
| 47 | |
| 48 | return result ? suggestedCallback(result) : emptyCallback(); |
| 49 | }, |
| 50 | |
| 51 | suggest: function(email, domains, secondLevelDomains, topLevelDomains, distanceFunction) { |
| 52 | email = email.toLowerCase(); |
| 53 | |
| 54 | var emailParts = this.splitEmail(email); |
| 55 | |
| 56 | if (secondLevelDomains && topLevelDomains) { |
| 57 | // If the email is a valid 2nd-level + top-level, do not suggest anything. |
| 58 | if (secondLevelDomains.indexOf(emailParts.secondLevelDomain) !== -1 && topLevelDomains.indexOf(emailParts.topLevelDomain) !== -1) { |
| 59 | return false; |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | var closestDomain = this.findClosestDomain(emailParts.domain, domains, distanceFunction, this.domainThreshold); |
| 64 | |
| 65 | if (closestDomain) { |
| 66 | if (closestDomain == emailParts.domain) { |
| 67 | // The email address exactly matches one of the supplied domains; do not return a suggestion. |
| 68 | return false; |
| 69 | } else { |
| 70 | // The email address closely matches one of the supplied domains; return a suggestion |
| 71 | return { address: emailParts.address, domain: closestDomain, full: emailParts.address + "@" + closestDomain }; |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | // The email address does not closely match one of the supplied domains |
| 76 | var closestSecondLevelDomain = this.findClosestDomain(emailParts.secondLevelDomain, secondLevelDomains, distanceFunction, this.secondLevelThreshold); |
| 77 | var closestTopLevelDomain = this.findClosestDomain(emailParts.topLevelDomain, topLevelDomains, distanceFunction, this.topLevelThreshold); |
| 78 | |
| 79 | if (emailParts.domain) { |
| 80 | closestDomain = emailParts.domain; |
| 81 | var rtrn = false; |
| 82 | |
| 83 | if(closestSecondLevelDomain && closestSecondLevelDomain != emailParts.secondLevelDomain) { |
| 84 | // The email address may have a mispelled second-level domain; return a suggestion |
| 85 | closestDomain = closestDomain.replace(emailParts.secondLevelDomain, closestSecondLevelDomain); |
| 86 | rtrn = true; |
| 87 | } |
| 88 | |
| 89 | if(closestTopLevelDomain && closestTopLevelDomain != emailParts.topLevelDomain && emailParts.secondLevelDomain !== '') { |
| 90 | // The email address may have a mispelled top-level domain; return a suggestion |
| 91 | closestDomain = closestDomain.replace(new RegExp(emailParts.topLevelDomain + "$"), closestTopLevelDomain); |
| 92 | rtrn = true; |
| 93 | } |
| 94 | |
| 95 | if (rtrn) { |
| 96 | return { address: emailParts.address, domain: closestDomain, full: emailParts.address + "@" + closestDomain }; |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | /* The email address exactly matches one of the supplied domains, does not closely |
| 101 | * match any domain and does not appear to simply have a mispelled top-level domain, |
| 102 | * or is an invalid email address; do not return a suggestion. |
| 103 | */ |
| 104 | return false; |
| 105 | }, |
| 106 | |
| 107 | findClosestDomain: function(domain, domains, distanceFunction, threshold) { |
| 108 | threshold = threshold || this.topLevelThreshold; |
| 109 | var dist; |
| 110 | var minDist = Infinity; |
| 111 | var closestDomain = null; |
| 112 | |
| 113 | if (!domain || !domains) { |
| 114 | return false; |
| 115 | } |
| 116 | if(!distanceFunction) { |
| 117 | distanceFunction = this.sift4Distance; |
| 118 | } |
| 119 | |
| 120 | for (var i = 0; i < domains.length; i++) { |
| 121 | if (domain === domains[i]) { |
| 122 | return domain; |
| 123 | } |
| 124 | dist = distanceFunction(domain, domains[i]); |
| 125 | if (dist < minDist) { |
| 126 | minDist = dist; |
| 127 | closestDomain = domains[i]; |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | if (minDist <= threshold && closestDomain !== null) { |
| 132 | return closestDomain; |
| 133 | } else { |
| 134 | return false; |
| 135 | } |
| 136 | }, |
| 137 | |
| 138 | sift4Distance: function(s1, s2, maxOffset) { |
| 139 | // sift4: https://siderite.blogspot.com/2014/11/super-fast-and-accurate-string-distance.html |
| 140 | if (maxOffset === undefined) { |
| 141 | maxOffset = 5; //default |
| 142 | } |
| 143 | |
| 144 | if (!s1||!s1.length) { |
| 145 | if (!s2) { |
| 146 | return 0; |
| 147 | } |
| 148 | return s2.length; |
| 149 | } |
| 150 | |
| 151 | if (!s2||!s2.length) { |
| 152 | return s1.length; |
| 153 | } |
| 154 | |
| 155 | var l1=s1.length; |
| 156 | var l2=s2.length; |
| 157 | |
| 158 | var c1 = 0; //cursor for string 1 |
| 159 | var c2 = 0; //cursor for string 2 |
| 160 | var lcss = 0; //largest common subsequence |
| 161 | var local_cs = 0; //local common substring |
| 162 | var trans = 0; //number of transpositions ('ab' vs 'ba') |
| 163 | var offset_arr=[]; //offset pair array, for computing the transpositions |
| 164 | |
| 165 | while ((c1 < l1) && (c2 < l2)) { |
| 166 | if (s1.charAt(c1) == s2.charAt(c2)) { |
| 167 | local_cs++; |
| 168 | var isTrans=false; |
| 169 | //see if current match is a transposition |
| 170 | var i=0; |
| 171 | while (i<offset_arr.length) { |
| 172 | var ofs=offset_arr[i]; |
| 173 | if (c1<=ofs.c1 || c2 <= ofs.c2) { |
| 174 | // when two matches cross, the one considered a transposition is the one with the largest difference in offsets |
| 175 | isTrans=Math.abs(c2-c1)>=Math.abs(ofs.c2-ofs.c1); |
| 176 | if (isTrans) |
| 177 | { |
| 178 | trans++; |
| 179 | } else |
| 180 | { |
| 181 | if (!ofs.trans) { |
| 182 | ofs.trans=true; |
| 183 | trans++; |
| 184 | } |
| 185 | } |
| 186 | break; |
| 187 | } else { |
| 188 | if (c1>ofs.c2 && c2>ofs.c1) { |
| 189 | offset_arr.splice(i,1); |
| 190 | } else { |
| 191 | i++; |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | offset_arr.push({ |
| 196 | c1:c1, |
| 197 | c2:c2, |
| 198 | trans:isTrans |
| 199 | }); |
| 200 | } else { |
| 201 | lcss+=local_cs; |
| 202 | local_cs=0; |
| 203 | if (c1!=c2) { |
| 204 | c1=c2=Math.min(c1,c2); //using min allows the computation of transpositions |
| 205 | } |
| 206 | //if matching characters are found, remove 1 from both cursors (they get incremented at the end of the loop) |
| 207 | //so that we can have only one code block handling matches |
| 208 | for (var j = 0; j < maxOffset && (c1+j<l1 || c2+j<l2); j++) { |
| 209 | if ((c1 + j < l1) && (s1.charAt(c1 + j) == s2.charAt(c2))) { |
| 210 | c1+= j-1; |
| 211 | c2--; |
| 212 | break; |
| 213 | } |
| 214 | if ((c2 + j < l2) && (s1.charAt(c1) == s2.charAt(c2 + j))) { |
| 215 | c1--; |
| 216 | c2+= j-1; |
| 217 | break; |
| 218 | } |
| 219 | } |
| 220 | } |
| 221 | c1++; |
| 222 | c2++; |
| 223 | // this covers the case where the last match is on the last token in list, so that it can compute transpositions correctly |
| 224 | if ((c1 >= l1) || (c2 >= l2)) { |
| 225 | lcss+=local_cs; |
| 226 | local_cs=0; |
| 227 | c1=c2=Math.min(c1,c2); |
| 228 | } |
| 229 | } |
| 230 | lcss+=local_cs; |
| 231 | return Math.round(Math.max(l1,l2)- lcss +trans); //add the cost of transpositions to the final result |
| 232 | }, |
| 233 | |
| 234 | splitEmail: function(email) { |
| 235 | email = email !== null ? (email.replace(/^\s*/, '').replace(/\s*$/, '')) : null; // trim() not exist in old IE! |
| 236 | var parts = email.split('@'); |
| 237 | |
| 238 | if (parts.length < 2) { |
| 239 | return false; |
| 240 | } |
| 241 | |
| 242 | for (var i = 0; i < parts.length; i++) { |
| 243 | if (parts[i] === '') { |
| 244 | return false; |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | var domain = parts.pop(); |
| 249 | var domainParts = domain.split('.'); |
| 250 | var sld = ''; |
| 251 | var tld = ''; |
| 252 | |
| 253 | if (domainParts.length === 0) { |
| 254 | // The address does not have a top-level domain |
| 255 | return false; |
| 256 | } else if (domainParts.length == 1) { |
| 257 | // The address has only a top-level domain (valid under RFC) |
| 258 | tld = domainParts[0]; |
| 259 | } else { |
| 260 | // The address has a domain and a top-level domain |
| 261 | sld = domainParts[0]; |
| 262 | for (var j = 1; j < domainParts.length; j++) { |
| 263 | tld += domainParts[j] + '.'; |
| 264 | } |
| 265 | tld = tld.substring(0, tld.length - 1); |
| 266 | } |
| 267 | |
| 268 | return { |
| 269 | topLevelDomain: tld, |
| 270 | secondLevelDomain: sld, |
| 271 | domain: domain, |
| 272 | address: parts.join('@') |
| 273 | }; |
| 274 | }, |
| 275 | |
| 276 | // Encode the email address to prevent XSS but leave in valid |
| 277 | // characters, following this official spec: |
| 278 | // http://en.wikipedia.org/wiki/Email_address#Syntax |
| 279 | encodeEmail: function(email) { |
| 280 | var result = encodeURI(email); |
| 281 | result = result.replace('%20', ' ').replace('%25', '%').replace('%5E', '^') |
| 282 | .replace('%60', '`').replace('%7B', '{').replace('%7C', '|') |
| 283 | .replace('%7D', '}'); |
| 284 | return result; |
| 285 | } |
| 286 | }; |
| 287 | |
| 288 | // Export the mailcheck object if we're in a CommonJS env (e.g. Node). |
| 289 | // Modeled off of Underscore.js. |
| 290 | if (typeof module !== 'undefined' && module.exports) { |
| 291 | module.exports = Mailcheck; |
| 292 | } |
| 293 | |
| 294 | // Support AMD style definitions |
| 295 | // Based on jQuery (see http://stackoverflow.com/a/17954882/1322410) |
| 296 | if (typeof define === "function" && define.amd) { |
| 297 | define("mailcheck", [], function() { |
| 298 | return Mailcheck; |
| 299 | }); |
| 300 | } |
| 301 | |
| 302 | if (typeof window !== 'undefined' && window.jQuery) { |
| 303 | (function($){ |
| 304 | $.fn.mailcheck = function(opts) { |
| 305 | var self = this; |
| 306 | if (opts.suggested) { |
| 307 | var oldSuggested = opts.suggested; |
| 308 | opts.suggested = function(result) { |
| 309 | oldSuggested(self, result); |
| 310 | }; |
| 311 | } |
| 312 | |
| 313 | if (opts.empty) { |
| 314 | var oldEmpty = opts.empty; |
| 315 | opts.empty = function() { |
| 316 | oldEmpty.call(null, self); |
| 317 | }; |
| 318 | } |
| 319 | |
| 320 | opts.email = this.val(); |
| 321 | Mailcheck.run(opts); |
| 322 | }; |
| 323 | })(jQuery); |
| 324 | } |
| 325 |