├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── autoload.php.dist ├── composer.json ├── phpunit.xml ├── src ├── Parse.php └── ParseOptions.php └── tests ├── ParseTest.php └── testspec.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # standard 2 | composer.lock 3 | 4 | # composer 5 | vendor/* 6 | 7 | # bin 8 | bin/* 9 | 10 | *.cache 11 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | # .scrutinizer.yml 2 | 3 | checks: 4 | php: 5 | code_rating: true 6 | duplication: true 7 | build: 8 | image: default-jammy 9 | tests: 10 | override: 11 | - 12 | command: 'XDEBUG_MODE=coverage bin/phpunit --coverage-clover=.coverage' 13 | coverage: 14 | file: '.coverage' 15 | format: 'clover' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | php: 6 | - 7.1 7 | - 7.2 8 | - 7.3 9 | 10 | before_script: 11 | - composer self-update 12 | - composer install 13 | 14 | script: bin/phpunit 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Matthew J. Mucklo 2 | Copyright (c) 2004-2013 Fabien Potencier (for code taken from symfony/symfony) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | email-parse [![Build Status](https://travis-ci.org/mmucklo/email-parse.svg?branch=master)](https://travis-ci.org/mmucklo/email-parse) 2 | =========== 3 | 4 | Email\Parse is a multiple (and single) batch email address parser that is reasonably RFC822 / RFC2822 compliant. 5 | 6 | It parses a list of 1 to n email addresses separated by space or comma 7 | 8 | Installation: 9 | ------------- 10 | Add this line to your composer.json "require" section: 11 | 12 | ### composer.json 13 | ```json 14 | "require": { 15 | ... 16 | "mmucklo/email-parse": "*" 17 | ``` 18 | 19 | Usage: 20 | ------ 21 | 22 | ```php 23 | use Email\Parse; 24 | 25 | $result = Parse::getInstance()->parse("a@aaa.com b@bbb.com"); 26 | ``` 27 | 28 | Notes: 29 | ====== 30 | This should be RFC 2822 compliant, although it will let a few obsolete RFC 822 addresses through such as `test"test"test@xyz.com` (note the quoted string in the middle of the address, which may be obsolete as of RFC 2822). However it wont allow escaping outside of quotes such as `test@test@xyz.com`. This would have to be written as `"test@test"@xyz.com` 31 | 32 | Here are a few other examples: 33 | 34 | ``` 35 | "John Q. Public" 36 | this.is.an.address@xyz.com 37 | how-about-an-ip@[10.0.10.2] 38 | how-about-comments(this is a comment!!)@xyz.com 39 | ``` 40 | 41 | #### Function Spec #### 42 | 43 | ```php 44 | /** 45 | * function parse($emails, $multiple = true, $encoding = 'UTF-8') 46 | * @param string $emails List of Email addresses separated by comma or space if multiple 47 | * @param bool $multiple (optional, default: true) Whether to parse for multiple email addresses or not 48 | * @param string $encoding (optional, default: 'UTF-8')The encoding if not 'UTF-8' 49 | * @return: see below: */ 50 | 51 | if ($multiple): 52 | array('success' => boolean, // whether totally successful or not 53 | 'reason' => string, // if unsuccessful, the reason why 54 | 'email_addresses' => 55 | array('address' => string, // the full address (not including comments) 56 | 'original_address' => string, // the full address including comments 57 | 'simple_address' => string, // simply local_part@domain_part (e.g. someone@somewhere.com) 58 | 'name' => string, // the name on the email if given (e.g.: John Q. Public), including any quotes 59 | 'name_parsed' => string, // the name on the email if given (e.g.: John Q. Public), excluding any quotes 60 | 'local_part' => string, // the local part (before the '@' sign - e.g. johnpublic) 61 | 'local_part_parsed' => string, // the local part (before the '@' sign - e.g. johnpublic), excluding any quotes 62 | 'domain' => string, // the domain after the '@' if given 63 | 'ip' => string, // the IP after the '@' if given 64 | 'domain_part' => string, // either domain or IP depending on what given 65 | 'invalid' => boolean, // if the email is valid or not 66 | 'invalid_reason' => string), // if the email is invalid, the reason why 67 | array( .... ) // the next email address matched 68 | ) 69 | else: 70 | array('address' => string, // the full address including comments 71 | 'name' => string, // the name on the email if given (e.g.: John Q. Public) 72 | 'local_part' => string, // the local part (before the '@' sign - e.g. johnpublic) 73 | 'domain' => string, // the domain after the '@' if given 74 | 'ip' => string, // the IP after the '@' if given 75 | 'invalid' => boolean, // if the email is valid or not 76 | 'invalid_reason' => string) // if the email is invalid, the reason why 77 | endif; 78 | ``` 79 | 80 | Other Examples: 81 | --------------- 82 | ```php 83 | $email = "\"J Doe\" "; 84 | $result = Email\Parse->getInstance()->parse($email, false); 85 | 86 | $result == array('address' => '"JD" ', 87 | 'original_address' => '"JD" ', 88 | 'name' => '"JD"', 89 | 'name_parsed' => 'J Doe', 90 | 'local_part' => 'johndoe', 91 | 'local_part_parsed' => 'johndoe', 92 | 'domain_part' => 'xyz.com', 93 | 'domain' => 'xyz.com', 94 | 'ip' => '', 95 | 'invalid' => false, 96 | 'invalid_reason' => ''); 97 | 98 | $emails = "testing@[10.0.10.45] testing@xyz.com, testing-"test...2"@xyz.com (comment)"; 99 | $result = Email\Parse->getInstance()->parse($emails); 100 | $result == array( 101 | 'success' => boolean true 102 | 'reason' => null 103 | 'email_addresses' => 104 | array( 105 | array( 106 | 'address' => 'testing@[10.0.10.45]', 107 | 'original_address' => 'testing@[10.0.10.45]', 108 | 'name' => '', 109 | 'name_parsed' => '', 110 | 'local_part' => 'testing', 111 | 'local_part_parsed' => 'testing', 112 | 'domain_part' => '10.0.10.45', 113 | 'domain' => '', 114 | 'ip' => '10.0.10.45', 115 | 'invalid' => false, 116 | 'invalid_reason' => ''), 117 | array( 118 | 'address' => 'testing@xyz.com', 119 | 'original_address' => 'testing@xyz.com', 120 | 'name' => '', 121 | 'name_parsed' => '', 122 | 'local_part' => 'testing', 123 | 'local_part' => 'testing', 124 | 'domain_part' => 'xyz.com', 125 | 'domain' => 'xyz.com', 126 | 'ip' => '', 127 | 'invalid' => false, 128 | 'invalid_reason' => '') 129 | array( 130 | 'address' => '"testing-test...2"@xyz.com', 131 | 'original_address' => 'testing-"test...2"@xyz.com (comment)', 132 | 'name' => '', 133 | 'name_parsed' => '', 134 | 'local_part' => '"testing-test2"', 135 | 'local_part_parsed' => 'testing-test...2', 136 | 'domain_part' => 'xyz.com', 137 | 'domain' => 'xyz.com', 138 | 'ip' => '', 139 | 'invalid' => false, 140 | 'invalid_reason' => '') 141 | ) 142 | ); 143 | ``` 144 | -------------------------------------------------------------------------------- /autoload.php.dist: -------------------------------------------------------------------------------- 1 | =') && gc_enabled()) { 4 | // Disabling Zend Garbage Collection to prevent segfaults with PHP5.4+ 5 | // https://bugs.php.net/bug.php?id=53976 6 | gc_disable(); 7 | } 8 | 9 | $loader = require_once __DIR__.'/vendor/autoload.php'; 10 | 11 | return $loader; 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmucklo/email-parse", 3 | "type": "library", 4 | "description": "email-parse a (reasonably) RFC822 / RF2822-compliant library for batch parsing multiple (and single) email addresses", 5 | "license": "MIT", 6 | "keywords": [ 7 | "email", 8 | "parse", 9 | "RFC822", 10 | "RFC2822", 11 | "email-parse", 12 | "multiple-email", 13 | "batch-email" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Matthew J. Mucklo", 18 | "email": "mmucklo@gmail.com" 19 | } 20 | ], 21 | "require-dev": { 22 | "friendsofphp/php-cs-fixer": "^3.65", 23 | "phpunit/phpunit": "^9.6", 24 | "symfony/yaml": "^7.2" 25 | }, 26 | "require": { 27 | "php": "^8.2", 28 | "ext-mbstring": "*", 29 | "laminas/laminas-validator": "^3.0", 30 | "psr/log": "^3.0", 31 | "symfony/polyfill-intl-idn": "^1.31" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Email\\": "src/" 36 | } 37 | }, 38 | "config": { 39 | "bin-dir": "bin" 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Email\\Tests\\": "tests/" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src/ 6 | 7 | 8 | ./tests/ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ./tests/ 19 | 20 | 21 | 22 | 23 | benchmark 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Parse.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 71 | $this->options = $options ?: new ParseOptions(['%', '!']); 72 | } 73 | 74 | /** 75 | * Allows for post-construct injection of a logger. 76 | * 77 | * @param LoggerInterface $logger (optional) Psr-compliant logger 78 | */ 79 | public function setLogger(LoggerInterface $logger) 80 | { 81 | $this->logger = $logger; 82 | } 83 | 84 | public function setOptions(ParseOptions $options) 85 | { 86 | $this->options = $options; 87 | } 88 | 89 | /** 90 | * @return ParseOptions 91 | */ 92 | public function getOptions() 93 | { 94 | return $this->options; 95 | } 96 | 97 | /** 98 | * Abstraction to prevent logging when there's no logger. 99 | * 100 | * @param mixed $level 101 | * @param string $message 102 | */ 103 | protected function log($level, $message) 104 | { 105 | if ($this->logger) { 106 | $this->logger->log($level, $message); 107 | } 108 | } 109 | 110 | /** 111 | * Parses a list of 1 to n email addresses separated by space or comma. 112 | * 113 | * This should be RFC 2822 compliant, although it will let a few obsolete 114 | * RFC 822 addresses through such as test"test"test@xyz.com (note 115 | * the quoted string in the middle of the address, which may be obsolete 116 | * as of RFC 2822). However it wont allow escaping outside of quotes 117 | * such as test\@test@xyz.com. This would have to be written 118 | * as "test\@test"@xyz.com 119 | * 120 | * Here are a few other examples: 121 | * 122 | * "John Q. Public" 123 | * this.is.an.address@xyz.com 124 | * how-about-an-ip@[10.0.10.2] 125 | * how-about-comments(this is a comment!!)@xyz.com 126 | * 127 | * @param string $emails List of Email addresses separated by comma or space if multiple 128 | * @param bool $multiple (optional, default: true) Whether to parse for multiple email addresses or not 129 | * @param string $encoding (optional, default: 'UTF-8') The encoding if not 'UTF-8' 130 | * 131 | * @return array if ($multiple): 132 | * array('success' => boolean, // whether totally successful or not 133 | * 'reason' => string, // if unsuccessful, the reason why 134 | * 'email_addresses' => 135 | * array('address' => string, // the full address (not including comments) 136 | * 'original_address' => string, // the full address including comments 137 | * 'simple_address' => string, // simply local_part@domain_part (e.g. someone@somewhere.com) 138 | * 'name' => string, // the name on the email if given (e.g.: John Q. Public), including any quotes 139 | * 'name_parsed' => string, // the name on the email if given (e.g.: John Q. Public), excluding any quotes 140 | * 'local_part' => string, // the local part (before the '@' sign - e.g. johnpublic) 141 | * 'local_part_parsed' => string, // the local part (before the '@' sign - e.g. johnpublic), excluding any quotes 142 | * 'domain' => string, // the domain after the '@' if given 143 | * 'ip' => string, // the IP after the '@' if given 144 | * 'domain_part' => string, // either domain or IP depending on what given 145 | * 'invalid' => boolean, // if the email is valid or not 146 | * 'invalid_reason' => string), // if the email is invalid, the reason why 147 | * array( .... ) // the next email address matched 148 | * ) 149 | * else: 150 | * array('address' => string, // the full address including comments 151 | * 'name' => string, // the name on the email if given (e.g.: John Q. Public) 152 | * 'local_part' => string, // the local part (before the '@' sign - e.g. johnpublic) 153 | * 'domain' => string, // the domain after the '@' if given 154 | * 'ip' => string, // the IP after the '@' if given 155 | * 'invalid' => boolean, // if the email is valid or not 156 | * 'invalid_reason' => string) // if the email is invalid, the reason why 157 | * endif; 158 | * 159 | * EXAMPLES: 160 | * $email = "\"J Doe\" "; 161 | * $result = Email\Parse->getInstance()->parse($email, false); 162 | * 163 | * $result == array('address' => '"JD" ', 164 | * 'original_address' => '"JD" ', 165 | * 'name' => '"JD"', 166 | * 'name_parsed' => 'J Doe', 167 | * 'local_part' => 'johndoe', 168 | * 'local_part_parsed' => 'johndoe', 169 | * 'domain_part' => 'xyz.com', 170 | * 'domain' => 'xyz.com', 171 | * 'ip' => '', 172 | * 'invalid' => false, 173 | * 'invalid_reason' => ''); 174 | * 175 | * $emails = "testing@[10.0.10.45] testing@xyz.com, testing-"test...2"@xyz.com (comment)"; 176 | * $result = Email\Parse->getInstance()->parse($emails); 177 | * $result == array( 178 | * 'success' => boolean true 179 | * 'reason' => null 180 | * 'email_addresses' => 181 | * array( 182 | * array( 183 | * 'address' => 'testing@[10.0.10.45]', 184 | * 'original_address' => 'testing@[10.0.10.45]', 185 | * 'name' => '', 186 | * 'name_parsed' => '', 187 | * 'local_part' => 'testing', 188 | * 'local_part_parsed' => 'testing', 189 | * 'domain_part' => '10.0.10.45', 190 | * 'domain' => '', 191 | * 'ip' => '10.0.10.45', 192 | * 'invalid' => false, 193 | * 'invalid_reason' => ''), 194 | * array( 195 | * 'address' => 'testing@xyz.com', 196 | * 'original_address' => 'testing@xyz.com', 197 | * 'name' => '', 198 | * 'name_parsed' => '', 199 | * 'local_part' => 'testing', 200 | * 'local_part' => 'testing', 201 | * 'domain_part' => 'xyz.com', 202 | * 'domain' => 'xyz.com', 203 | * 'ip' => '', 204 | * 'invalid' => false, 205 | * 'invalid_reason' => '') 206 | * array( 207 | * 'address' => '"testing-test...2"@xyz.com', 208 | * 'original_address' => 'testing-"test...2"@xyz.com (comment)', 209 | * 'name' => '', 210 | * 'name_parsed' => '', 211 | * 'local_part' => '"testing-test2"', 212 | * 'local_part_parsed' => 'testing-test...2', 213 | * 'domain_part' => 'xyz.com', 214 | * 'domain' => 'xyz.com', 215 | * 'ip' => '', 216 | * 'invalid' => false, 217 | * 'invalid_reason' => '') 218 | * ) 219 | * ); 220 | */ 221 | public function parse($emails, $multiple = true, $encoding = 'UTF-8') 222 | { 223 | $emailAddresses = []; 224 | 225 | // Variables to be used during email address collection 226 | $emailAddress = $this->buildEmailAddressArray(); 227 | 228 | $success = true; 229 | $reason = null; 230 | 231 | // Current state of the parser 232 | $state = self::STATE_TRIM; 233 | 234 | // Current sub state (this is for when we get to the xyz@somewhere.com email address itself) 235 | $subState = self::STATE_START; 236 | $commentNestLevel = 0; 237 | 238 | $len = mb_strlen($emails, $encoding); 239 | if (0 == $len) { 240 | $success = false; 241 | $reason = 'No emails passed in'; 242 | } 243 | $curChar = null; 244 | for ($i = 0; $i < $len; ++$i) { 245 | $prevChar = $curChar; // Previous Charater 246 | $curChar = mb_substr($emails, $i, 1, $encoding); // Current Character 247 | switch ($state) { 248 | case self::STATE_SKIP_AHEAD: 249 | // Skip ahead is set when a bad email address is encountered 250 | // It's supposed to skip to the next delimiter and continue parsing from there 251 | if ($multiple && 252 | (' ' == $curChar || 253 | "\r" == $curChar || 254 | "\n" == $curChar || 255 | "\t" == $curChar || 256 | ',' == $curChar)) { 257 | $state = self::STATE_END_ADDRESS; 258 | } else { 259 | $emailAddress['original_address'] .= $curChar; 260 | } 261 | 262 | break; 263 | /* @noinspection PhpMissingBreakStatementInspection */ 264 | case self::STATE_TRIM: 265 | if (' ' == $curChar || 266 | "\r" == $curChar || 267 | "\n" == $curChar || 268 | "\t" == $curChar) { 269 | break; 270 | } else { 271 | $state = self::STATE_ADDRESS; 272 | if ('"' == $curChar) { 273 | $emailAddress['original_address'] .= $curChar; 274 | $state = self::STATE_QUOTE; 275 | break; 276 | } elseif ('(' == $curChar) { 277 | $emailAddress['original_address'] .= $curChar; 278 | $state = self::STATE_COMMENT; 279 | break; 280 | } 281 | // Fall through to next case self::STATE_ADDRESS on purpose here 282 | } 283 | // Fall through 284 | // no break 285 | case self::STATE_ADDRESS: 286 | if (',' != $curChar || !$multiple) { 287 | $emailAddress['original_address'] .= $curChar; 288 | } 289 | 290 | if ('(' == $curChar) { 291 | // Handle comment 292 | $state = self::STATE_COMMENT; 293 | $commentNestLevel = 1; 294 | break; 295 | } elseif (',' == $curChar) { 296 | // Handle Comma 297 | if ($multiple && (self::STATE_DOMAIN == $subState || self::STATE_AFTER_DOMAIN == $subState)) { 298 | // If we're already in the domain part, this should be the end of the address 299 | $state = self::STATE_END_ADDRESS; 300 | break; 301 | } else { 302 | $emailAddress['invalid'] = true; 303 | if ($multiple || ($i + 5) >= $len) { 304 | $emailAddress['invalid_reason'] = 'Misplaced Comma or missing "@" symbol'; 305 | } else { 306 | $emailAddress['invalid_reason'] = 'Comma not permitted - only one email address allowed'; 307 | } 308 | } 309 | } elseif (' ' == $curChar || 310 | "\t" == $curChar || "\r" == $curChar || 311 | "\n" == $curChar) { 312 | // Handle Whitespace 313 | 314 | // Look ahead for comments after the address 315 | $foundComment = false; 316 | for ($j = ($i + 1); $j < $len; ++$j) { 317 | $lookAheadChar = mb_substr($emails, $j, 1, $encoding); 318 | if ('(' == $lookAheadChar) { 319 | $foundComment = true; 320 | break; 321 | } elseif (' ' != $lookAheadChar && 322 | "\t" != $lookAheadChar && 323 | "\r" != $lookAheadChar && 324 | "\n" != $lookAheadChar) { 325 | break; 326 | } 327 | } 328 | // Check if there's a comment found ahead 329 | if ($foundComment) { 330 | if (self::STATE_DOMAIN == $subState) { 331 | $subState = self::STATE_AFTER_DOMAIN; 332 | } elseif (self::STATE_LOCAL_PART == $subState) { 333 | $emailAddress['invalid'] = true; 334 | $emailAddress['invalid_reason'] = 'Email Address contains whitespace'; 335 | } 336 | } elseif (self::STATE_DOMAIN == $subState || self::STATE_AFTER_DOMAIN == $subState) { 337 | // If we're already in the domain part, this should be the end of the whole address 338 | $state = self::STATE_END_ADDRESS; 339 | break; 340 | } else { 341 | if (self::STATE_LOCAL_PART == $subState) { 342 | $emailAddress['invalid'] = true; 343 | $emailAddress['invalid_reason'] = 'Email address contains whitespace'; 344 | } else { 345 | // If the previous section was a quoted string, then use that for the name 346 | $this->handleQuote($emailAddress); 347 | $emailAddress['name_parsed'] .= $curChar; 348 | } 349 | } 350 | } elseif ('<' == $curChar) { 351 | // Start of the local part 352 | if (self::STATE_LOCAL_PART == $subState || self::STATE_DOMAIN == $subState) { 353 | $emailAddress['invalid'] = true; 354 | $emailAddress['invalid_reason'] = 'Email address contains multiple opening "<" (either a typo or multiple emails that need to be separated by a comma or space)'; 355 | } else { 356 | // Here should be the start of the local part for sure everything else then is part of the name 357 | $subState = self::STATE_LOCAL_PART; 358 | $emailAddress['special_char_in_substate'] = null; 359 | $this->handleQuote($emailAddress); 360 | } 361 | } elseif ('>' == $curChar) { 362 | // should be end of domain part 363 | if (self::STATE_DOMAIN != $subState) { 364 | $emailAddress['invalid'] = true; 365 | $emailAddress['invalid_reason'] = "Did not find domain name before a closing '>'"; 366 | } else { 367 | $subState = self::STATE_AFTER_DOMAIN; 368 | } 369 | } elseif ('"' == $curChar) { 370 | // If we hit a quote - change to the quote state, unless it's in the domain, in which case it's error 371 | if (self::STATE_DOMAIN == $subState || self::STATE_AFTER_DOMAIN == $subState) { 372 | $emailAddress['invalid'] = true; 373 | $emailAddress['invalid_reason'] = 'Quote \'"\' found where it shouldn\'t be'; 374 | } else { 375 | $state = self::STATE_QUOTE; 376 | } 377 | } elseif ('@' == $curChar) { 378 | // Handle '@' sign 379 | if (self::STATE_DOMAIN == $subState) { 380 | $emailAddress['invalid'] = true; 381 | $emailAddress['invalid_reason'] = "Multiple at '@' symbols in email address"; 382 | } elseif (self::STATE_AFTER_DOMAIN == $subState) { 383 | $emailAddress['invalid'] = true; 384 | $emailAddress['invalid_reason'] = "Stray at '@' symbol found after domain name"; 385 | } elseif (null !== $emailAddress['special_char_in_substate']) { 386 | $emailAddress['invalid'] = true; 387 | $emailAddress['invalid_reason'] = "Invalid character found in email address local part: '{$emailAddress['special_char_in_substate']}'"; 388 | } else { 389 | $subState = self::STATE_DOMAIN; 390 | if ($emailAddress['address_temp'] && $emailAddress['quote_temp']) { 391 | $emailAddress['invalid'] = true; 392 | $emailAddress['invalid_reason'] = 'Something went wrong during parsing.'; 393 | $this->log('error', "Email\\Parse->parse - Something went wrong during parsing:\n\$i: {$i}\n\$emailAddress['address_temp']: {$emailAddress['address_temp']}\n\$emailAddress['quote_temp']: {$emailAddress['quote_temp']}\nEmails: {$emails}\n\$curChar: {$curChar}"); 394 | } elseif ($emailAddress['quote_temp']) { 395 | $emailAddress['local_part_parsed'] = $emailAddress['quote_temp']; 396 | $emailAddress['quote_temp'] = ''; 397 | $emailAddress['local_part_quoted'] = true; 398 | } elseif ($emailAddress['address_temp']) { 399 | $emailAddress['local_part_parsed'] = $emailAddress['address_temp']; 400 | $emailAddress['address_temp'] = ''; 401 | $emailAddress['local_part_quoted'] = $emailAddress['address_temp_quoted']; 402 | $emailAddress['address_temp_quoted'] = false; 403 | $emailAddress['address_temp_period'] = 0; 404 | } 405 | } 406 | } elseif ('[' == $curChar) { 407 | // Setup square bracket special handling if appropriate 408 | if (self::STATE_DOMAIN != $subState) { 409 | $emailAddress['invalid'] = true; 410 | $emailAddress['invalid_reason'] = "Invalid character '[' in email address"; 411 | } 412 | $state = self::STATE_SQUARE_BRACKET; 413 | } elseif ('.' == $curChar) { 414 | // Handle periods specially 415 | if ('.' == $prevChar) { 416 | $emailAddress['invalid'] = true; 417 | $emailAddress['invalid_reason'] = "Email address should not contain two dots '.' in a row"; 418 | } elseif (self::STATE_LOCAL_PART == $subState) { 419 | if (!$emailAddress['local_part_parsed']) { 420 | $emailAddress['invalid'] = true; 421 | $emailAddress['invalid_reason'] = "Email address can not start with '.'"; 422 | } else { 423 | $emailAddress['local_part_parsed'] .= $curChar; 424 | } 425 | } elseif (self::STATE_DOMAIN == $subState) { 426 | $emailAddress['domain'] .= $curChar; 427 | } elseif (self::STATE_AFTER_DOMAIN == $subState) { 428 | $emailAddress['invalid'] = true; 429 | $emailAddress['invalid_reason'] = "Stray period '.' found after domain of email address"; 430 | } elseif (self::STATE_START == $subState) { 431 | if ($emailAddress['quote_temp']) { 432 | $emailAddress['address_temp'] .= $emailAddress['quote_temp']; 433 | $emailAddress['address_temp_quoted'] = true; 434 | $emailAddress['quote_temp'] = ''; 435 | } 436 | $emailAddress['address_temp'] .= $curChar; 437 | ++$emailAddress['address_temp_period']; 438 | } else { 439 | // Strict RFC 2822 - require all periods to be quoted in other parts of the string 440 | $emailAddress['invalid'] = true; 441 | $emailAddress['invalid_reason'] = 'Stray period found in email address. If the period is part of a person\'s name, it must appear in double quotes - e.g. "John Q. Public". Otherwise, an email address shouldn\'t begin with a period.'; 442 | } 443 | } elseif (preg_match('/[A-Za-z0-9_\-!#$%&\'*+\/=?^`{|}~]/', $curChar)) { 444 | // see RFC 2822 445 | 446 | if (isset($this->options->getBannedChars()[$curChar])) { 447 | $emailAddress['invalid'] = true; 448 | $emailAddress['invalid_reason'] = "This character is not allowed in email addresses submitted (please put in quotes if needed): '{$curChar}'"; 449 | } elseif (('/' == $curChar || '|' == $curChar) && 450 | !$emailAddress['local_part_parsed'] && !$emailAddress['address_temp'] && !$emailAddress['quote_temp']) { 451 | $emailAddress['invalid'] = true; 452 | $emailAddress['invalid_reason'] = "This character is not allowed in the beginning of an email addresses (please put in quotes if needed): '{$curChar}'"; 453 | } elseif (self::STATE_LOCAL_PART == $subState) { 454 | // Legitimate character - Determine where to append based on the current 'substate' 455 | 456 | if ($emailAddress['quote_temp']) { 457 | $emailAddress['local_part_parsed'] .= $emailAddress['quote_temp']; 458 | $emailAddress['quote_temp'] = ''; 459 | $emailAddress['local_part_quoted'] = true; 460 | } 461 | $emailAddress['local_part_parsed'] .= $curChar; 462 | } elseif (self::STATE_NAME == $subState) { 463 | if ($emailAddress['quote_temp']) { 464 | $emailAddress['name_parsed'] .= $emailAddress['quote_temp']; 465 | $emailAddress['quote_temp'] = ''; 466 | $emailAddress['name_quoted'] = true; 467 | } 468 | $emailAddress['name_parsed'] .= $curChar; 469 | } elseif (self::STATE_DOMAIN == $subState) { 470 | $emailAddress['domain'] .= $curChar; 471 | } else { 472 | if ($emailAddress['quote_temp']) { 473 | $emailAddress['address_temp'] .= $emailAddress['quote_temp']; 474 | $emailAddress['address_temp_quoted'] = true; 475 | $emailAddress['quote_temp'] = ''; 476 | } 477 | $emailAddress['address_temp'] .= $curChar; 478 | } 479 | } else { 480 | if (self::STATE_DOMAIN == $subState) { 481 | try { 482 | // Test by trying to encode the current character into Punycode 483 | // Punycode should match the traditional domain name subset of characters 484 | if (preg_match('/[a-z0-9\-]/', idn_to_ascii($curChar))) { 485 | $emailAddress['domain'] .= $curChar; 486 | } else { 487 | $emailAddress['invalid'] = true; 488 | } 489 | } catch (\Exception $e) { 490 | $this->log('warning', "Email\\Parse->parse - exception trying to convert character '{$curChar}' to punycode\n\$emailAddress['original_address']: {$emailAddress['original_address']}\n\$emails: {$emails}"); 491 | $emailAddress['invalid'] = true; 492 | } 493 | if ($emailAddress['invalid']) { 494 | $emailAddress['invalid_reason'] = "Invalid character found in domain of email address (please put in quotes if needed): '{$curChar}'"; 495 | } 496 | } elseif (self::STATE_START === $subState) { 497 | if ($emailAddress['quote_temp']) { 498 | $emailAddress['address_temp'] .= $emailAddress['quote_temp']; 499 | $emailAddress['address_temp_quoted'] = true; 500 | $emailAddress['quote_temp'] = ''; 501 | } 502 | $emailAddress['special_char_in_substate'] = $curChar; 503 | $emailAddress['address_temp'] .= $curChar; 504 | } elseif (self::STATE_NAME === $subState) { 505 | if ($emailAddress['quote_temp']) { 506 | $emailAddress['name_parsed'] .= $emailAddress['quote_temp']; 507 | $emailAddress['quote_temp'] = ''; 508 | $emailAddress['name_quoted'] = true; 509 | } 510 | $emailAddress['special_char_in_substate'] = $curChar; 511 | $emailAddress['name_parsed'] .= $curChar; 512 | } else { 513 | $emailAddress['invalid'] = true; 514 | $emailAddress['invalid_reason'] = "Invalid character found in email address (please put in quotes if needed): '{$curChar}'"; 515 | } 516 | } 517 | break; 518 | case self::STATE_SQUARE_BRACKET: 519 | // Handle square bracketed IP addresses such as [10.0.10.2] 520 | $emailAddress['original_address'] .= $curChar; 521 | if (']' == $curChar) { 522 | $subState = self::STATE_AFTER_DOMAIN; 523 | $state = self::STATE_ADDRESS; 524 | } elseif (preg_match('/[0-9\.]/', $curChar)) { 525 | $emailAddress['ip'] .= $curChar; 526 | } else { 527 | $emailAddress['invalid'] = true; 528 | $emailAddress['invalid_reason'] = "Invalid Character '{$curChar}' in what seemed to be an IP Address"; 529 | } 530 | break; 531 | case self::STATE_QUOTE: 532 | // Handle quoted strings 533 | $emailAddress['original_address'] .= $curChar; 534 | if ('"' == $curChar) { 535 | $backslashCount = 0; 536 | for ($j = $i; $j >= 0; --$j) { 537 | if ('\\' == mb_substr($emails, $j, 1, $encoding)) { 538 | ++$backslashCount; 539 | } else { 540 | break; 541 | } 542 | } 543 | if ($backslashCount && 1 == $backslashCount % 2) { 544 | // This is a quoted quote 545 | $emailAddress['quote_temp'] .= $curChar; 546 | } else { 547 | $state = self::STATE_ADDRESS; 548 | } 549 | } else { 550 | $emailAddress['quote_temp'] .= $curChar; 551 | } 552 | break; 553 | case self::STATE_COMMENT: 554 | // Handle comments and nesting thereof 555 | $emailAddress['original_address'] .= $curChar; 556 | if (')' == $curChar) { 557 | --$commentNestLevel; 558 | if ($commentNestLevel <= 0) { 559 | $state = self::STATE_ADDRESS; 560 | } 561 | } elseif ('(' == $curChar) { 562 | ++$commentNestLevel; 563 | } 564 | break; 565 | default: 566 | // Shouldn't ever get here - what is $state? 567 | $emailAddress['original_address'] .= $curChar; 568 | $emailAddress['invalid'] = true; 569 | $emailAddress['invalid_reason'] = 'Error during parsing'; 570 | $this->log('error', "Email\\Parse->parse - error during parsing - \$state: {$state}\n\$subState: {$subState}\$i: {$i}\n\$curChar: {$curChar}"); 571 | break; 572 | } 573 | 574 | // if there's a $emailAddress['original_address'] and the state is set to STATE_END_ADDRESS 575 | if (self::STATE_END_ADDRESS == $state && strlen($emailAddress['original_address']) > 0) { 576 | $invalid = $this->addAddress( 577 | $emailAddresses, 578 | $emailAddress, 579 | $encoding, 580 | $i 581 | ); 582 | 583 | if ($invalid) { 584 | if (!$success) { 585 | $reason = 'Invalid email addresses'; 586 | } else { 587 | $reason = 'Invalid email address'; 588 | $success = false; 589 | } 590 | } 591 | 592 | // Reset all local variables used during parsing 593 | $emailAddress = $this->buildEmailAddressArray(); 594 | $subState = self::STATE_START; 595 | $state = self::STATE_TRIM; 596 | } 597 | 598 | if ($emailAddress['invalid']) { 599 | $this->log('debug', "Email\\Parse->parse - invalid - {$emailAddress['invalid_reason']}\n\$emailAddress['original_address'] {$emailAddress['original_address']}\n\$emails: {$emails}"); 600 | $state = self::STATE_SKIP_AHEAD; 601 | } 602 | } 603 | 604 | // Catch all the various fall-though places 605 | if (!$emailAddress['invalid'] && $emailAddress['quote_temp'] && self::STATE_QUOTE == $state) { 606 | $emailAddress['invalid'] = true; 607 | $emailAddress['invalid_reason'] = 'No ending quote: \'"\''; 608 | } 609 | if (!$emailAddress['invalid'] && $emailAddress['quote_temp'] && self::STATE_COMMENT == $state) { 610 | $emailAddress['invalid'] = true; 611 | $emailAddress['invalid_reason'] = 'No closing parenthesis: \')\''; 612 | } 613 | if (!$emailAddress['invalid'] && $emailAddress['quote_temp'] && self::STATE_SQUARE_BRACKET == $state) { 614 | $emailAddress['invalid'] = true; 615 | $emailAddress['invalid_reason'] = 'No closing square bracket: \']\''; 616 | } 617 | if (!$emailAddress['invalid'] && $emailAddress['address_temp'] || $emailAddress['quote_temp']) { 618 | $this->log('error', "Email\\Parse->parse - corruption during parsing - leftovers:\n\$i: {$i}\n\$emailAddress['address_temp']: {$emailAddress['address_temp']}\n\$emailAddress['quote_temp']: {$emailAddress['quote_temp']}\nEmails: {$emails}"); 619 | $emailAddress['invalid'] = true; 620 | $emailAddress['invalid_reason'] = 'Incomplete address'; 621 | if (!$success) { 622 | $reason = 'Invalid email addresses'; 623 | } else { 624 | $reason = 'Invalid email address'; 625 | $success = false; 626 | } 627 | } 628 | 629 | // Did we find no email addresses at all? 630 | if (!$emailAddress['invalid'] && !count($emailAddresses) && (!$emailAddress['original_address'] || !$emailAddress['local_part_parsed'])) { 631 | $success = false; 632 | $reason = 'No email addresses found'; 633 | if (!$multiple) { 634 | $emailAddress['invalid'] = true; 635 | $emailAddress['invalid_reason'] = 'No email address found'; 636 | $this->addAddress( 637 | $emailAddresses, 638 | $emailAddress, 639 | $encoding, 640 | $i 641 | ); 642 | } 643 | } elseif ($emailAddress['original_address']) { 644 | $invalid = $this->addAddress( 645 | $emailAddresses, 646 | $emailAddress, 647 | $encoding, 648 | $i 649 | ); 650 | if ($invalid) { 651 | if (!$success) { 652 | $reason = 'Invalid email addresses'; 653 | } else { 654 | $reason = 'Invalid email address'; 655 | $success = false; 656 | } 657 | } 658 | } 659 | if ($multiple) { 660 | return ['success' => $success, 'reason' => $reason, 'email_addresses' => $emailAddresses]; 661 | } else { 662 | return $emailAddresses[0]; 663 | } 664 | } 665 | 666 | /** 667 | * Handles the case of a quoted name. 668 | */ 669 | private function handleQuote(array &$emailAddress) 670 | { 671 | if ($emailAddress['quote_temp']) { 672 | $emailAddress['name_parsed'] .= $emailAddress['quote_temp']; 673 | $emailAddress['name_quoted'] = true; 674 | $emailAddress['quote_temp'] = ''; 675 | } elseif ($emailAddress['address_temp']) { 676 | $emailAddress['name_parsed'] .= $emailAddress['address_temp']; 677 | $emailAddress['name_quoted'] = $emailAddress['address_temp_quoted']; 678 | $emailAddress['address_temp_quoted'] = false; 679 | $emailAddress['address_temp'] = ''; 680 | if ($emailAddress['address_temp_period'] > 0) { 681 | $emailAddress['invalid'] = true; 682 | $emailAddress['invalid_reason'] = 'Periods within the name of an email address must appear in quotes, such as "John Q. Public" '; 683 | } 684 | } 685 | } 686 | 687 | /** 688 | * Helper function for creating a blank email address array used by Email\Parse->parse. 689 | */ 690 | private function buildEmailAddressArray() 691 | { 692 | $emailAddress = ['original_address' => '', 693 | 'name_parsed' => '', 694 | 'local_part_parsed' => '', 695 | 'domain' => '', 696 | 'ip' => '', 697 | 'invalid' => false, 698 | 'invalid_reason' => null, 699 | 'local_part_quoted' => false, 700 | 'name_quoted' => false, 701 | 'address_temp_quoted' => false, 702 | 'quote_temp' => '', 703 | 'address_temp' => '', 704 | 'address_temp_period' => 0, 705 | 'special_char_in_substate' => null, 706 | ]; 707 | 708 | return $emailAddress; 709 | } 710 | 711 | /** 712 | * Does a bunch of additional validation on the email address parts contained in $emailAddress 713 | * Then adds it to $emailAdddresses. 714 | * 715 | * @return mixed 716 | */ 717 | private function addAddress( 718 | &$emailAddresses, 719 | &$emailAddress, 720 | $encoding, 721 | $i 722 | ) { 723 | if (!$emailAddress['invalid']) { 724 | if ($emailAddress['address_temp'] || $emailAddress['quote_temp']) { 725 | $emailAddress['invalid'] = true; 726 | $emailAddress['invalid_reason'] = 'Incomplete address'; 727 | $this->log('error', "Email\\Parse->addAddress - corruption during parsing - leftovers:\n\$i: {$i}\n\$emailAddress['address_temp'] : {$emailAddress['address_temp']}\n\$emailAddress['quote_temp']: {$emailAddress['quote_temp']}\n"); 728 | } elseif ($emailAddress['ip'] && $emailAddress['domain']) { 729 | // Error - this should never occur 730 | $emailAddress['invalid'] = true; 731 | $emailAddress['invalid_reason'] = 'Confusion during parsing'; 732 | $this->log('error', "Email\\Parse->addAddress - both an IP address '{$emailAddress['ip']}' and a domain '{$emailAddress['domain']}' found for the email address '{$emailAddress['original_address']}'\n"); 733 | } elseif ($emailAddress['ip'] || ($emailAddress['domain'] && preg_match('/\d+\.\d+\.\d+\.\d+/', $emailAddress['domain']))) { 734 | // also test if the current domain looks like an IP address 735 | 736 | if ($emailAddress['domain']) { 737 | // Likely an IP address if we get here 738 | 739 | $emailAddress['ip'] = $emailAddress['domain']; 740 | $emailAddress['domain'] = null; 741 | } 742 | if (!$this->ipValidator) { 743 | $this->ipValidator = new Ip(); 744 | } 745 | try { 746 | if (!$this->ipValidator->isValid($emailAddress['ip'])) { 747 | $emailAddress['invalid'] = true; 748 | $emailAddress['invalid_reason'] = 'IP address invalid: \''.$emailAddress['ip'].'\' does not appear to be a valid IP address'; 749 | } elseif (preg_match('/192\.168\.\d+\.\d+/', $emailAddress['ip']) || 750 | preg_match('/172\.(1[6-9]|2[0-9]|3[0-2])\.\d+\.\d+/', $emailAddress['ip']) || 751 | preg_match('/10\.\d+\.\d+\.\d+/', $emailAddress['ip'])) { 752 | $emailAddress['invalid'] = true; 753 | $emailAddress['invalid_reason'] = 'IP address invalid (private): '.$emailAddress['ip']; 754 | } elseif (preg_match('/169\.254\.\d+\.\d+/', $emailAddress['ip'])) { 755 | $emailAddress['invalid'] = true; 756 | $emailAddress['invalid_reason'] = 'IP address invalid (APIPA): '.$emailAddress['ip']; 757 | } 758 | } catch (\Exception $e) { 759 | $emailAddress['invalid'] = true; 760 | $emailAddress['invalid_reason'] = 'IP address invalid: '.$emailAddress['ip']; 761 | } 762 | } elseif ($emailAddress['domain']) { 763 | // Check for IDNA 764 | if (max(array_keys(count_chars($emailAddress['domain'], 1))) > 127) { 765 | try { 766 | $emailAddress['domain'] = idn_to_ascii($emailAddress['domain']); 767 | } catch (\Exception $e) { 768 | $emailAddress['invalid'] = true; 769 | $emailAddress['invalid_reason'] = "Can't convert domain {$emailAddress['domain']} to punycode"; 770 | } 771 | } 772 | 773 | $result = $this->validateDomainName($emailAddress['domain']); 774 | if (!$result['valid']) { 775 | $emailAddress['invalid'] = true; 776 | $emailAddress['invalid_reason'] = isset($result['reason']) ? 'Domain invalid: '.$result['reason'] : 'Domain invalid for some unknown reason'; 777 | } 778 | } 779 | } 780 | 781 | // Prepare some of the fields needed 782 | $emailAddress['name_parsed'] = rtrim($emailAddress['name_parsed']); 783 | $emailAddress['original_address'] = rtrim($emailAddress['original_address']); 784 | $name = $emailAddress['name_quoted'] ? "\"{$emailAddress['name_parsed']}\"" : $emailAddress['name_parsed']; 785 | $localPart = $emailAddress['local_part_quoted'] ? "\"{$emailAddress['local_part_parsed']}\"" : $emailAddress['local_part_parsed']; 786 | $domainPart = $emailAddress['ip'] ? '['.$emailAddress['ip'].']' : $emailAddress['domain']; 787 | 788 | if (!$emailAddress['invalid']) { 789 | if (0 == mb_strlen($domainPart, $encoding)) { 790 | $emailAddress['invalid'] = true; 791 | $emailAddress['invalid_reason'] = 'Email address needs a domain after the \'@\''; 792 | } elseif (mb_strlen($localPart, $encoding) > 63) { 793 | $emailAddress['invalid'] = true; 794 | $emailAddress['invalid_reason'] = 'Email address before the \'@\' can not be greater than 63 characters'; 795 | } elseif ((mb_strlen($localPart, $encoding) + mb_strlen($domainPart, $encoding) + 1) > 254) { 796 | $emailAddress['invalid'] = true; 797 | $emailAddress['invalid_reason'] = 'Email addresses can not be greater than 254 characters'; 798 | } 799 | } 800 | 801 | // Build the email address hash 802 | $emailAddrDef = ['address' => '', 803 | 'simple_address' => '', 804 | 'original_address' => rtrim($emailAddress['original_address']), 805 | 'name' => $name, 806 | 'name_parsed' => $emailAddress['name_parsed'], 807 | 'local_part' => $localPart, 808 | 'local_part_parsed' => $emailAddress['local_part_parsed'], 809 | 'domain_part' => $domainPart, 810 | 'domain' => $emailAddress['domain'], 811 | 'ip' => $emailAddress['ip'], 812 | 'invalid' => $emailAddress['invalid'], 813 | 'invalid_reason' => $emailAddress['invalid_reason'], ]; 814 | 815 | // Build the proper address by hand (has comments stripped out and should have quotes in the proper places) 816 | if (!$emailAddrDef['invalid']) { 817 | $emailAddrDef['simple_address'] = "{$emailAddrDef['local_part']}@{$emailAddrDef['domain_part']}"; 818 | $properAddress = $emailAddrDef['name'] ? "{$emailAddrDef['name']} <{$emailAddrDef['local_part']}@{$emailAddrDef['domain_part']}>" : $emailAddrDef['simple_address']; 819 | $emailAddrDef['address'] = $properAddress; 820 | } 821 | 822 | $emailAddresses[] = $emailAddrDef; 823 | 824 | return $emailAddrDef['invalid']; 825 | } 826 | 827 | /** 828 | * Determines whether the domain name is valid. 829 | * 830 | * @param string $domain The domain name to validate 831 | * @param string $encoding The encoding of the string (if not UTF-8) 832 | * 833 | * @return array array('valid' => boolean: whether valid or not, 834 | * 'reason' => string: if not valid, the reason why); 835 | */ 836 | protected function validateDomainName($domain, $encoding = 'UTF-8') 837 | { 838 | if (mb_strlen($domain, $encoding) > 255) { 839 | return ['valid' => false, 'reason' => 'Domain name too long']; 840 | } else { 841 | $origEncoding = mb_regex_encoding(); 842 | mb_regex_encoding($encoding); 843 | $parts = mb_split('\\.', $domain); 844 | mb_regex_encoding($origEncoding); 845 | foreach ($parts as $part) { 846 | if (mb_strlen($part, $encoding) > 63) { 847 | return ['valid' => false, 'reason' => "Domain name part '{$part}' too long"]; 848 | } 849 | if (!preg_match('/^[a-zA-Z0-9\-]+$/', $part)) { 850 | return ['valid' => false, 'reason' => "Domain name '{$domain}' can only contain letters a through z, numbers 0 through 9 and hyphen. The part '{$part}' contains characters outside of that range."]; 851 | } 852 | if ('-' == mb_substr($part, 0, 1, $encoding) || '-' == mb_substr($part, mb_strlen($part) - 1, 1, $encoding)) { 853 | return ['valid' => false, 'reason' => "Parts of the domain name '{$domain}' can not start or end with '-'. This part does: {$part}"]; 854 | } 855 | } 856 | } 857 | 858 | // @TODO - possibly check DNS / MX records for domain to make sure it exists? 859 | return ['valid' => true]; 860 | } 861 | } 862 | -------------------------------------------------------------------------------- /src/ParseOptions.php: -------------------------------------------------------------------------------- 1 | setBannedChars($bannedChars); 13 | } 14 | } 15 | 16 | public function setBannedChars(array $bannedChars) 17 | { 18 | $this->bannedChars = []; 19 | foreach ($bannedChars as $bannedChar) { 20 | $this->bannedChars[$bannedChar] = true; 21 | } 22 | } 23 | 24 | /** 25 | * @return array|null 26 | */ 27 | public function getBannedChars() 28 | { 29 | return $this->bannedChars; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/ParseTest.php: -------------------------------------------------------------------------------- 1 | assertSame($result, Parse::getInstance()->parse($emails, $multiple)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/testspec.yml: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | - 4 | emails: t.name@asdf.ghjkl.com 5 | multiple: true 6 | result: 7 | success: true 8 | reason: null 9 | email_addresses: 10 | - 11 | address: t.name@asdf.ghjkl.com 12 | simple_address: t.name@asdf.ghjkl.com 13 | original_address: t.name@asdf.ghjkl.com 14 | name: '' 15 | name_parsed: '' 16 | local_part: t.name 17 | local_part_parsed: t.name 18 | domain_part: asdf.ghjkl.com 19 | domain: asdf.ghjkl.com 20 | ip: '' 21 | invalid: false 22 | invalid_reason: null 23 | - 24 | emails: tname@asdf.ghjkl.com 25 | multiple: true 26 | result: 27 | success: true 28 | reason: null 29 | email_addresses: 30 | - 31 | address: tname@asdf.ghjkl.com 32 | simple_address: tname@asdf.ghjkl.com 33 | original_address: tname@asdf.ghjkl.com 34 | name: '' 35 | name_parsed: '' 36 | local_part: tname 37 | local_part_parsed: tname 38 | domain_part: asdf.ghjkl.com 39 | domain: asdf.ghjkl.com 40 | ip: '' 41 | invalid: false 42 | invalid_reason: null 43 | - 44 | emails: tname@asdf.ghjkl.com 45 | multiple: false 46 | result: 47 | address: tname@asdf.ghjkl.com 48 | simple_address: tname@asdf.ghjkl.com 49 | original_address: tname@asdf.ghjkl.com 50 | name: '' 51 | name_parsed: '' 52 | local_part: tname 53 | local_part_parsed: tname 54 | domain_part: asdf.ghjkl.com 55 | domain: asdf.ghjkl.com 56 | ip: '' 57 | invalid: false 58 | invalid_reason: null 59 | - 60 | emails: 'Testing Name ' 61 | multiple: true 62 | result: 63 | success: true 64 | reason: null 65 | email_addresses: 66 | - 67 | address: 'Testing Name ' 68 | simple_address: tname@asdf.ghjkl.com 69 | original_address: 'Testing Name ' 70 | name: 'Testing Name' 71 | name_parsed: 'Testing Name' 72 | local_part: tname 73 | local_part_parsed: tname 74 | domain_part: asdf.ghjkl.com 75 | domain: asdf.ghjkl.com 76 | ip: '' 77 | invalid: false 78 | invalid_reason: null 79 | - 80 | emails: 'Testing Name ' 81 | multiple: false 82 | result: 83 | address: 'Testing Name ' 84 | simple_address: tname@asdf.ghjkl.com 85 | original_address: 'Testing Name ' 86 | name: 'Testing Name' 87 | name_parsed: 'Testing Name' 88 | local_part: tname 89 | local_part_parsed: tname 90 | domain_part: asdf.ghjkl.com 91 | domain: asdf.ghjkl.com 92 | ip: '' 93 | invalid: false 94 | invalid_reason: null 95 | - 96 | emails: 'Testing D Name ' 97 | multiple: true 98 | result: 99 | success: true 100 | reason: null 101 | email_addresses: 102 | - 103 | address: 'Testing D Name ' 104 | simple_address: tname@asdf.ghjkl.com 105 | original_address: 'Testing D Name ' 106 | name: 'Testing D Name' 107 | name_parsed: 'Testing D Name' 108 | local_part: tname 109 | local_part_parsed: tname 110 | domain_part: asdf.ghjkl.com 111 | domain: asdf.ghjkl.com 112 | ip: '' 113 | invalid: false 114 | invalid_reason: null 115 | - 116 | emails: 'Testing D Name ' 117 | multiple: false 118 | result: 119 | address: 'Testing D Name ' 120 | simple_address: tname@asdf.ghjkl.com 121 | original_address: 'Testing D Name ' 122 | name: 'Testing D Name' 123 | name_parsed: 'Testing D Name' 124 | local_part: tname 125 | local_part_parsed: tname 126 | domain_part: asdf.ghjkl.com 127 | domain: asdf.ghjkl.com 128 | ip: '' 129 | invalid: false 130 | invalid_reason: null 131 | - 132 | emails: 'Testing D. Name ' 133 | multiple: true 134 | result: 135 | success: false 136 | reason: 'Invalid email address' 137 | email_addresses: 138 | - 139 | address: '' 140 | simple_address: '' 141 | original_address: 'Testing D. Name' 142 | name: 'Testing D.' 143 | name_parsed: 'Testing D.' 144 | local_part: '' 145 | local_part_parsed: '' 146 | domain_part: '' 147 | domain: '' 148 | ip: '' 149 | invalid: true 150 | invalid_reason: 'Periods within the name of an email address must appear in quotes, such as "John Q. Public" ' 151 | - 152 | address: tname@asdf.ghjkl.com 153 | simple_address: tname@asdf.ghjkl.com 154 | original_address: '' 155 | name: '' 156 | name_parsed: '' 157 | local_part: tname 158 | local_part_parsed: tname 159 | domain_part: asdf.ghjkl.com 160 | domain: asdf.ghjkl.com 161 | ip: '' 162 | invalid: false 163 | invalid_reason: null 164 | - 165 | emails: 'Testing D. Name ' 166 | multiple: false 167 | result: 168 | address: '' 169 | simple_address: '' 170 | original_address: 'Testing D. Name ' 171 | name: 'Testing D.' 172 | name_parsed: 'Testing D.' 173 | local_part: '' 174 | local_part_parsed: '' 175 | domain_part: '' 176 | domain: '' 177 | ip: '' 178 | invalid: true 179 | invalid_reason: 'Periods within the name of an email address must appear in quotes, such as "John Q. Public" ' 180 | - 181 | emails: 'test.testing@asdf.ghjkl.com test.testing2@asdf.ghjkl.com' 182 | multiple: true 183 | result: 184 | success: true 185 | reason: null 186 | email_addresses: 187 | - 188 | address: test.testing@asdf.ghjkl.com 189 | simple_address: test.testing@asdf.ghjkl.com 190 | original_address: test.testing@asdf.ghjkl.com 191 | name: '' 192 | name_parsed: '' 193 | local_part: test.testing 194 | local_part_parsed: test.testing 195 | domain_part: asdf.ghjkl.com 196 | domain: asdf.ghjkl.com 197 | ip: '' 198 | invalid: false 199 | invalid_reason: null 200 | - 201 | address: test.testing2@asdf.ghjkl.com 202 | simple_address: test.testing2@asdf.ghjkl.com 203 | original_address: test.testing2@asdf.ghjkl.com 204 | name: '' 205 | name_parsed: '' 206 | local_part: test.testing2 207 | local_part_parsed: test.testing2 208 | domain_part: asdf.ghjkl.com 209 | domain: asdf.ghjkl.com 210 | ip: '' 211 | invalid: false 212 | invalid_reason: null 213 | - 214 | emails: '"Testing D. Name" ' 215 | multiple: true 216 | result: 217 | success: true 218 | reason: null 219 | email_addresses: 220 | - 221 | address: '"Testing D. Name" ' 222 | simple_address: tname@asdf.ghjkl.com 223 | original_address: '"Testing D. Name" ' 224 | name: '"Testing D. Name"' 225 | name_parsed: 'Testing D. Name' 226 | local_part: tname 227 | local_part_parsed: tname 228 | domain_part: asdf.ghjkl.com 229 | domain: asdf.ghjkl.com 230 | ip: '' 231 | invalid: false 232 | invalid_reason: null 233 | - 234 | emails: '"Testing D. Name" ' 235 | multiple: false 236 | result: 237 | address: '"Testing D. Name" ' 238 | simple_address: tname@asdf.ghjkl.com 239 | original_address: '"Testing D. Name" ' 240 | name: '"Testing D. Name"' 241 | name_parsed: 'Testing D. Name' 242 | local_part: tname 243 | local_part_parsed: tname 244 | domain_part: asdf.ghjkl.com 245 | domain: asdf.ghjkl.com 246 | ip: '' 247 | invalid: false 248 | invalid_reason: null 249 | - 250 | emails: '' 251 | multiple: true 252 | result: 253 | success: true 254 | reason: null 255 | email_addresses: 256 | - 257 | address: tname@asdf.ghjkl.com 258 | simple_address: tname@asdf.ghjkl.com 259 | original_address: '' 260 | name: '' 261 | name_parsed: '' 262 | local_part: tname 263 | local_part_parsed: tname 264 | domain_part: asdf.ghjkl.com 265 | domain: asdf.ghjkl.com 266 | ip: '' 267 | invalid: false 268 | invalid_reason: null 269 | - 270 | emails: '' 271 | multiple: false 272 | result: 273 | address: tname@asdf.ghjkl.com 274 | simple_address: tname@asdf.ghjkl.com 275 | original_address: '' 276 | name: '' 277 | name_parsed: '' 278 | local_part: tname 279 | local_part_parsed: tname 280 | domain_part: asdf.ghjkl.com 281 | domain: asdf.ghjkl.com 282 | ip: '' 283 | invalid: false 284 | invalid_reason: null 285 | - 286 | emails: '<.tname@asdf.ghjkl.com>' 287 | multiple: true 288 | result: 289 | success: false 290 | reason: 'Invalid email address' 291 | email_addresses: 292 | - 293 | address: '' 294 | simple_address: '' 295 | original_address: '<.tname@asdf.ghjkl.com>' 296 | name: '' 297 | name_parsed: '' 298 | local_part: '' 299 | local_part_parsed: '' 300 | domain_part: '' 301 | domain: '' 302 | ip: '' 303 | invalid: true 304 | invalid_reason: 'Email address can not start with ''.''' 305 | - 306 | emails: '<.tname@asdf.ghjkl.com>' 307 | multiple: false 308 | result: 309 | address: '' 310 | simple_address: '' 311 | original_address: '<.tname@asdf.ghjkl.com>' 312 | name: '' 313 | name_parsed: '' 314 | local_part: '' 315 | local_part_parsed: '' 316 | domain_part: '' 317 | domain: '' 318 | ip: '' 319 | invalid: true 320 | invalid_reason: 'Email address can not start with ''.''' 321 | - 322 | emails: '"test .s set .set"@asdf.ghjkl.com' 323 | multiple: true 324 | result: 325 | success: true 326 | reason: null 327 | email_addresses: 328 | - 329 | address: '"test .s set .set"@asdf.ghjkl.com' 330 | simple_address: '"test .s set .set"@asdf.ghjkl.com' 331 | original_address: '"test .s set .set"@asdf.ghjkl.com' 332 | name: '' 333 | name_parsed: '' 334 | local_part: '"test .s set .set"' 335 | local_part_parsed: 'test .s set .set' 336 | domain_part: asdf.ghjkl.com 337 | domain: asdf.ghjkl.com 338 | ip: '' 339 | invalid: false 340 | invalid_reason: null 341 | - 342 | emails: '"test .s set .set"@asdf.ghjkl.com' 343 | multiple: false 344 | result: 345 | address: '"test .s set .set"@asdf.ghjkl.com' 346 | simple_address: '"test .s set .set"@asdf.ghjkl.com' 347 | original_address: '"test .s set .set"@asdf.ghjkl.com' 348 | name: '' 349 | name_parsed: '' 350 | local_part: '"test .s set .set"' 351 | local_part_parsed: 'test .s set .set' 352 | domain_part: asdf.ghjkl.com 353 | domain: asdf.ghjkl.com 354 | ip: '' 355 | invalid: false 356 | invalid_reason: null 357 | - 358 | emails: '' 359 | multiple: true 360 | result: 361 | success: true 362 | reason: null 363 | email_addresses: 364 | - 365 | address: t.name@asdf.ghjkl.com 366 | simple_address: t.name@asdf.ghjkl.com 367 | original_address: '' 368 | name: '' 369 | name_parsed: '' 370 | local_part: t.name 371 | local_part_parsed: t.name 372 | domain_part: asdf.ghjkl.com 373 | domain: asdf.ghjkl.com 374 | ip: '' 375 | invalid: false 376 | invalid_reason: null 377 | - 378 | emails: '' 379 | multiple: false 380 | result: 381 | address: t.name@asdf.ghjkl.com 382 | simple_address: t.name@asdf.ghjkl.com 383 | original_address: '' 384 | name: '' 385 | name_parsed: '' 386 | local_part: t.name 387 | local_part_parsed: t.name 388 | domain_part: asdf.ghjkl.com 389 | domain: asdf.ghjkl.com 390 | ip: '' 391 | invalid: false 392 | invalid_reason: null 393 | - 394 | emails: '' 395 | multiple: true 396 | result: 397 | success: true 398 | reason: null 399 | email_addresses: 400 | - 401 | address: t.name.@asdf.ghjkl.com 402 | simple_address: t.name.@asdf.ghjkl.com 403 | original_address: '' 404 | name: '' 405 | name_parsed: '' 406 | local_part: t.name. 407 | local_part_parsed: t.name. 408 | domain_part: asdf.ghjkl.com 409 | domain: asdf.ghjkl.com 410 | ip: '' 411 | invalid: false 412 | invalid_reason: null 413 | - 414 | emails: '' 415 | multiple: false 416 | result: 417 | address: t.name.@asdf.ghjkl.com 418 | simple_address: t.name.@asdf.ghjkl.com 419 | original_address: '' 420 | name: '' 421 | name_parsed: '' 422 | local_part: t.name. 423 | local_part_parsed: t.name. 424 | domain_part: asdf.ghjkl.com 425 | domain: asdf.ghjkl.com 426 | ip: '' 427 | invalid: false 428 | invalid_reason: null 429 | - 430 | emails: 'tname@asdf.ghjkl.com, tname@asdf.ghjkl.com, tname-test1@asdf.ghjkl.com' 431 | multiple: true 432 | result: 433 | success: true 434 | reason: null 435 | email_addresses: 436 | - 437 | address: tname@asdf.ghjkl.com 438 | simple_address: tname@asdf.ghjkl.com 439 | original_address: tname@asdf.ghjkl.com 440 | name: '' 441 | name_parsed: '' 442 | local_part: tname 443 | local_part_parsed: tname 444 | domain_part: asdf.ghjkl.com 445 | domain: asdf.ghjkl.com 446 | ip: '' 447 | invalid: false 448 | invalid_reason: null 449 | - 450 | address: tname@asdf.ghjkl.com 451 | simple_address: tname@asdf.ghjkl.com 452 | original_address: tname@asdf.ghjkl.com 453 | name: '' 454 | name_parsed: '' 455 | local_part: tname 456 | local_part_parsed: tname 457 | domain_part: asdf.ghjkl.com 458 | domain: asdf.ghjkl.com 459 | ip: '' 460 | invalid: false 461 | invalid_reason: null 462 | - 463 | address: tname-test1@asdf.ghjkl.com 464 | simple_address: tname-test1@asdf.ghjkl.com 465 | original_address: tname-test1@asdf.ghjkl.com 466 | name: '' 467 | name_parsed: '' 468 | local_part: tname-test1 469 | local_part_parsed: tname-test1 470 | domain_part: asdf.ghjkl.com 471 | domain: asdf.ghjkl.com 472 | ip: '' 473 | invalid: false 474 | invalid_reason: null 475 | - 476 | emails: 'tname@asdf.ghjkl.com, tname@asdf.ghjkl.com, tname-test1@asdf.ghjkl.com' 477 | multiple: false 478 | result: 479 | address: '' 480 | simple_address: '' 481 | original_address: 'tname@asdf.ghjkl.com, tname@asdf.ghjkl.com, tname-test1@asdf.ghjkl.com' 482 | name: '' 483 | name_parsed: '' 484 | local_part: tname 485 | local_part_parsed: tname 486 | domain_part: asdf.ghjkl.com 487 | domain: asdf.ghjkl.com 488 | ip: '' 489 | invalid: true 490 | invalid_reason: 'Comma not permitted - only one email address allowed' 491 | - 492 | emails: 'tnam e@asdf.g asdfa hjkl.com, tn''''''ame@asdf.ghjkl.com, tname-test1@asdf.ghjkl.com' 493 | multiple: true 494 | result: 495 | success: false 496 | reason: 'Invalid email address' 497 | email_addresses: 498 | - 499 | address: 'tnam ' 500 | simple_address: e@asdf.g 501 | original_address: 'tnam e@asdf.g' 502 | name: tnam 503 | name_parsed: tnam 504 | local_part: e 505 | local_part_parsed: e 506 | domain_part: asdf.g 507 | domain: asdf.g 508 | ip: '' 509 | invalid: false 510 | invalid_reason: null 511 | - 512 | address: '' 513 | simple_address: '' 514 | original_address: 'asdfa hjkl.com' 515 | name: asdfa 516 | name_parsed: asdfa 517 | local_part: '' 518 | local_part_parsed: '' 519 | domain_part: '' 520 | domain: '' 521 | ip: '' 522 | invalid: true 523 | invalid_reason: 'Misplaced Comma or missing "@" symbol' 524 | - 525 | address: 'tn''''''ame@asdf.ghjkl.com' 526 | simple_address: 'tn''''''ame@asdf.ghjkl.com' 527 | original_address: 'tn''''''ame@asdf.ghjkl.com' 528 | name: '' 529 | name_parsed: '' 530 | local_part: 'tn''''''ame' 531 | local_part_parsed: 'tn''''''ame' 532 | domain_part: asdf.ghjkl.com 533 | domain: asdf.ghjkl.com 534 | ip: '' 535 | invalid: false 536 | invalid_reason: null 537 | - 538 | address: tname-test1@asdf.ghjkl.com 539 | simple_address: tname-test1@asdf.ghjkl.com 540 | original_address: tname-test1@asdf.ghjkl.com 541 | name: '' 542 | name_parsed: '' 543 | local_part: tname-test1 544 | local_part_parsed: tname-test1 545 | domain_part: asdf.ghjkl.com 546 | domain: asdf.ghjkl.com 547 | ip: '' 548 | invalid: false 549 | invalid_reason: null 550 | - 551 | emails: 'tnam e@asdf.g asdfa hjkl.com, tn''''''ame@asdf.ghjkl.com, tname-test1@asdf.ghjkl.com' 552 | multiple: false 553 | result: 554 | address: 'tnam ' 555 | simple_address: e@asdf.g 556 | original_address: 'tnam e@asdf.g' 557 | name: tnam 558 | name_parsed: tnam 559 | local_part: e 560 | local_part_parsed: e 561 | domain_part: asdf.g 562 | domain: asdf.g 563 | ip: '' 564 | invalid: false 565 | invalid_reason: null 566 | - 567 | emails: 'Testing D Name tname@asdf.ghjkl.com tname-test1@asdf.ghjkl.com' 568 | multiple: true 569 | result: 570 | success: true 571 | reason: null 572 | email_addresses: 573 | - 574 | address: 'Testing D Name ' 575 | simple_address: tname@asdf.ghjkl.com 576 | original_address: 'Testing D Name ' 577 | name: 'Testing D Name' 578 | name_parsed: 'Testing D Name' 579 | local_part: tname 580 | local_part_parsed: tname 581 | domain_part: asdf.ghjkl.com 582 | domain: asdf.ghjkl.com 583 | ip: '' 584 | invalid: false 585 | invalid_reason: null 586 | - 587 | address: tname@asdf.ghjkl.com 588 | simple_address: tname@asdf.ghjkl.com 589 | original_address: tname@asdf.ghjkl.com 590 | name: '' 591 | name_parsed: '' 592 | local_part: tname 593 | local_part_parsed: tname 594 | domain_part: asdf.ghjkl.com 595 | domain: asdf.ghjkl.com 596 | ip: '' 597 | invalid: false 598 | invalid_reason: null 599 | - 600 | address: tname-test1@asdf.ghjkl.com 601 | simple_address: tname-test1@asdf.ghjkl.com 602 | original_address: tname-test1@asdf.ghjkl.com 603 | name: '' 604 | name_parsed: '' 605 | local_part: tname-test1 606 | local_part_parsed: tname-test1 607 | domain_part: asdf.ghjkl.com 608 | domain: asdf.ghjkl.com 609 | ip: '' 610 | invalid: false 611 | invalid_reason: null 612 | - 613 | emails: 'Testing D Name tname@asdf.ghjkl.com tname-test1@asdf.ghjkl.com' 614 | multiple: false 615 | result: 616 | address: 'Testing D Name ' 617 | simple_address: tname@asdf.ghjkl.com 618 | original_address: 'Testing D Name ' 619 | name: 'Testing D Name' 620 | name_parsed: 'Testing D Name' 621 | local_part: tname 622 | local_part_parsed: tname 623 | domain_part: asdf.ghjkl.com 624 | domain: asdf.ghjkl.com 625 | ip: '' 626 | invalid: false 627 | invalid_reason: null 628 | - 629 | emails: 'Testing D Name (comment) tn(comment1)ame@asdf.gh(comment2)jkl.com tname-test1(comment3)@asdf.ghjkl.com' 630 | multiple: true 631 | result: 632 | success: true 633 | reason: null 634 | email_addresses: 635 | - 636 | address: 'Testing D Name ' 637 | simple_address: tname@asdf.ghjkl.com 638 | original_address: 'Testing D Name (comment)' 639 | name: 'Testing D Name' 640 | name_parsed: 'Testing D Name' 641 | local_part: tname 642 | local_part_parsed: tname 643 | domain_part: asdf.ghjkl.com 644 | domain: asdf.ghjkl.com 645 | ip: '' 646 | invalid: false 647 | invalid_reason: null 648 | - 649 | address: tname@asdf.ghjkl.com 650 | simple_address: tname@asdf.ghjkl.com 651 | original_address: tn(comment1)ame@asdf.gh(comment2)jkl.com 652 | name: '' 653 | name_parsed: '' 654 | local_part: tname 655 | local_part_parsed: tname 656 | domain_part: asdf.ghjkl.com 657 | domain: asdf.ghjkl.com 658 | ip: '' 659 | invalid: false 660 | invalid_reason: null 661 | - 662 | address: tname-test1@asdf.ghjkl.com 663 | simple_address: tname-test1@asdf.ghjkl.com 664 | original_address: tname-test1(comment3)@asdf.ghjkl.com 665 | name: '' 666 | name_parsed: '' 667 | local_part: tname-test1 668 | local_part_parsed: tname-test1 669 | domain_part: asdf.ghjkl.com 670 | domain: asdf.ghjkl.com 671 | ip: '' 672 | invalid: false 673 | invalid_reason: null 674 | - 675 | emails: 'Testing D Name (comment) tn(comment1)ame@asdf.gh(comment2)jkl.com tname-test1(comment3)@asdf.ghjkl.com' 676 | multiple: false 677 | result: 678 | address: 'Testing D Name ' 679 | simple_address: tname@asdf.ghjkl.com 680 | original_address: 'Testing D Name (comment)' 681 | name: 'Testing D Name' 682 | name_parsed: 'Testing D Name' 683 | local_part: tname 684 | local_part_parsed: tname 685 | domain_part: asdf.ghjkl.com 686 | domain: asdf.ghjkl.com 687 | ip: '' 688 | invalid: false 689 | invalid_reason: null 690 | - 691 | emails: 'Tes!@#$@$&*&%(*ti)ng D Name (comment) tna(m!@#($(!(^)$)#)!^%#&*%^#)mment1e@asdf.gh(comment2)jkl.com tname-test1(comment3)@asdf.ghjkl.com' 692 | multiple: true 693 | result: 694 | success: false 695 | reason: 'Invalid email address' 696 | email_addresses: 697 | - 698 | address: '' 699 | simple_address: '' 700 | original_address: 'Tes!@#$@$&*&%(*ti)ng' 701 | name: '' 702 | name_parsed: '' 703 | local_part: '' 704 | local_part_parsed: '' 705 | domain_part: '' 706 | domain: '' 707 | ip: '' 708 | invalid: true 709 | invalid_reason: 'This character is not allowed in email addresses submitted (please put in quotes if needed): ''!''' 710 | - 711 | address: 'D Name ' 712 | simple_address: tname@asdf.ghjkl.com 713 | original_address: 'D Name (comment)' 714 | name: 'D Name' 715 | name_parsed: 'D Name' 716 | local_part: tname 717 | local_part_parsed: tname 718 | domain_part: asdf.ghjkl.com 719 | domain: asdf.ghjkl.com 720 | ip: '' 721 | invalid: false 722 | invalid_reason: null 723 | - 724 | address: tnamment1e@asdf.ghjkl.com 725 | simple_address: tnamment1e@asdf.ghjkl.com 726 | original_address: 'tna(m!@#($(!(^)$)#)!^%#&*%^#)mment1e@asdf.gh(comment2)jkl.com' 727 | name: '' 728 | name_parsed: '' 729 | local_part: tnamment1e 730 | local_part_parsed: tnamment1e 731 | domain_part: asdf.ghjkl.com 732 | domain: asdf.ghjkl.com 733 | ip: '' 734 | invalid: false 735 | invalid_reason: null 736 | - 737 | address: tname-test1@asdf.ghjkl.com 738 | simple_address: tname-test1@asdf.ghjkl.com 739 | original_address: tname-test1(comment3)@asdf.ghjkl.com 740 | name: '' 741 | name_parsed: '' 742 | local_part: tname-test1 743 | local_part_parsed: tname-test1 744 | domain_part: asdf.ghjkl.com 745 | domain: asdf.ghjkl.com 746 | ip: '' 747 | invalid: false 748 | invalid_reason: null 749 | - 750 | emails: 'Tes!@#$@$&*&%(*ti)ng D Name (comment) tna(m!@#($(!(^)$)#)!^%#&*%^#)mment1e@asdf.gh(comment2)jkl.com tname-test1(comment3)@asdf.ghjkl.com' 751 | multiple: false 752 | result: 753 | address: '' 754 | simple_address: '' 755 | original_address: 'Tes!@#$@$&*&%(*ti)ng D Name (comment) tna(m!@#($(!(^)$)#)!^%#&*%^#)mment1e@asdf.gh(comment2)jkl.com tname-test1(comment3)@asdf.ghjkl.com' 756 | name: '' 757 | name_parsed: '' 758 | local_part: '' 759 | local_part_parsed: '' 760 | domain_part: '' 761 | domain: '' 762 | ip: '' 763 | invalid: true 764 | invalid_reason: 'This character is not allowed in email addresses submitted (please put in quotes if needed): ''!''' 765 | - 766 | emails: 'tname@[10.0.10.45] tname@asdf.ghjkl.com, tname-test2@asdf.ghjkl.com' 767 | multiple: true 768 | result: 769 | success: false 770 | reason: 'Invalid email address' 771 | email_addresses: 772 | - 773 | address: '' 774 | simple_address: '' 775 | original_address: 'tname@[10.0.10.45]' 776 | name: '' 777 | name_parsed: '' 778 | local_part: tname 779 | local_part_parsed: tname 780 | domain_part: '[10.0.10.45]' 781 | domain: '' 782 | ip: 10.0.10.45 783 | invalid: true 784 | invalid_reason: 'IP address invalid (private): 10.0.10.45' 785 | - 786 | address: tname@asdf.ghjkl.com 787 | simple_address: tname@asdf.ghjkl.com 788 | original_address: tname@asdf.ghjkl.com 789 | name: '' 790 | name_parsed: '' 791 | local_part: tname 792 | local_part_parsed: tname 793 | domain_part: asdf.ghjkl.com 794 | domain: asdf.ghjkl.com 795 | ip: '' 796 | invalid: false 797 | invalid_reason: null 798 | - 799 | address: tname-test2@asdf.ghjkl.com 800 | simple_address: tname-test2@asdf.ghjkl.com 801 | original_address: tname-test2@asdf.ghjkl.com 802 | name: '' 803 | name_parsed: '' 804 | local_part: tname-test2 805 | local_part_parsed: tname-test2 806 | domain_part: asdf.ghjkl.com 807 | domain: asdf.ghjkl.com 808 | ip: '' 809 | invalid: false 810 | invalid_reason: null 811 | - 812 | emails: 'tname@[10.0.10.45] tname@asdf.ghjkl.com, tname-test2@asdf.ghjkl.com' 813 | multiple: false 814 | result: 815 | address: '' 816 | simple_address: '' 817 | original_address: 'tname@[10.0.10.45]' 818 | name: '' 819 | name_parsed: '' 820 | local_part: tname 821 | local_part_parsed: tname 822 | domain_part: '[10.0.10.45]' 823 | domain: '' 824 | ip: 10.0.10.45 825 | invalid: true 826 | invalid_reason: 'IP address invalid (private): 10.0.10.45' 827 | - 828 | emails: 't"na"me@[10.0.10.45] tname@asdf.ghjkl.com, tname-test2@asdf.ghjkl.com' 829 | multiple: true 830 | result: 831 | success: false 832 | reason: 'Invalid email address' 833 | email_addresses: 834 | - 835 | address: '' 836 | simple_address: '' 837 | original_address: 't"na"me@[10.0.10.45]' 838 | name: '' 839 | name_parsed: '' 840 | local_part: '"tname"' 841 | local_part_parsed: tname 842 | domain_part: '[10.0.10.45]' 843 | domain: '' 844 | ip: 10.0.10.45 845 | invalid: true 846 | invalid_reason: 'IP address invalid (private): 10.0.10.45' 847 | - 848 | address: tname@asdf.ghjkl.com 849 | simple_address: tname@asdf.ghjkl.com 850 | original_address: tname@asdf.ghjkl.com 851 | name: '' 852 | name_parsed: '' 853 | local_part: tname 854 | local_part_parsed: tname 855 | domain_part: asdf.ghjkl.com 856 | domain: asdf.ghjkl.com 857 | ip: '' 858 | invalid: false 859 | invalid_reason: null 860 | - 861 | address: tname-test2@asdf.ghjkl.com 862 | simple_address: tname-test2@asdf.ghjkl.com 863 | original_address: tname-test2@asdf.ghjkl.com 864 | name: '' 865 | name_parsed: '' 866 | local_part: tname-test2 867 | local_part_parsed: tname-test2 868 | domain_part: asdf.ghjkl.com 869 | domain: asdf.ghjkl.com 870 | ip: '' 871 | invalid: false 872 | invalid_reason: null 873 | - 874 | emails: 't"na"me@[10.0.10.45] tname@asdf.ghjkl.com, tname-test2@asdf.ghjkl.com' 875 | multiple: false 876 | result: 877 | address: '' 878 | simple_address: '' 879 | original_address: 't"na"me@[10.0.10.45]' 880 | name: '' 881 | name_parsed: '' 882 | local_part: '"tname"' 883 | local_part_parsed: tname 884 | domain_part: '[10.0.10.45]' 885 | domain: '' 886 | ip: 10.0.10.45 887 | invalid: true 888 | invalid_reason: 'IP address invalid (private): 10.0.10.45' 889 | - 890 | emails: 't(comment with spaces !!!)name@[10.0.10.45] tname@asdf.ghjkl.com, tname-test2@asdf.ghjkl.com' 891 | multiple: true 892 | result: 893 | success: false 894 | reason: 'Invalid email address' 895 | email_addresses: 896 | - 897 | address: '' 898 | simple_address: '' 899 | original_address: 't(comment with spaces !!!)name@[10.0.10.45]' 900 | name: '' 901 | name_parsed: '' 902 | local_part: tname 903 | local_part_parsed: tname 904 | domain_part: '[10.0.10.45]' 905 | domain: '' 906 | ip: 10.0.10.45 907 | invalid: true 908 | invalid_reason: 'IP address invalid (private): 10.0.10.45' 909 | - 910 | address: tname@asdf.ghjkl.com 911 | simple_address: tname@asdf.ghjkl.com 912 | original_address: tname@asdf.ghjkl.com 913 | name: '' 914 | name_parsed: '' 915 | local_part: tname 916 | local_part_parsed: tname 917 | domain_part: asdf.ghjkl.com 918 | domain: asdf.ghjkl.com 919 | ip: '' 920 | invalid: false 921 | invalid_reason: null 922 | - 923 | address: tname-test2@asdf.ghjkl.com 924 | simple_address: tname-test2@asdf.ghjkl.com 925 | original_address: tname-test2@asdf.ghjkl.com 926 | name: '' 927 | name_parsed: '' 928 | local_part: tname-test2 929 | local_part_parsed: tname-test2 930 | domain_part: asdf.ghjkl.com 931 | domain: asdf.ghjkl.com 932 | ip: '' 933 | invalid: false 934 | invalid_reason: null 935 | - 936 | emails: 't(comment with spaces !!!)name@[10.0.10.45] tname@asdf.ghjkl.com, tname-test2@asdf.ghjkl.com' 937 | multiple: false 938 | result: 939 | address: '' 940 | simple_address: '' 941 | original_address: 't(comment with spaces !!!)name@[10.0.10.45]' 942 | name: '' 943 | name_parsed: '' 944 | local_part: tname 945 | local_part_parsed: tname 946 | domain_part: '[10.0.10.45]' 947 | domain: '' 948 | ip: 10.0.10.45 949 | invalid: true 950 | invalid_reason: 'IP address invalid (private): 10.0.10.45' 951 | - 952 | emails: testing@tūdaliņ.lv 953 | multiple: true 954 | result: 955 | success: true 956 | reason: null 957 | email_addresses: 958 | - 959 | address: testing@xn--tdali-d8a8w.lv 960 | simple_address: testing@xn--tdali-d8a8w.lv 961 | original_address: testing@tūdaliņ.lv 962 | name: '' 963 | name_parsed: '' 964 | local_part: testing 965 | local_part_parsed: testing 966 | domain_part: xn--tdali-d8a8w.lv 967 | domain: xn--tdali-d8a8w.lv 968 | ip: '' 969 | invalid: false 970 | invalid_reason: null 971 | - 972 | emails: testing@tūdaliņ.lv 973 | multiple: false 974 | result: 975 | address: testing@xn--tdali-d8a8w.lv 976 | simple_address: testing@xn--tdali-d8a8w.lv 977 | original_address: testing@tūdaliņ.lv 978 | name: '' 979 | name_parsed: '' 980 | local_part: testing 981 | local_part_parsed: testing 982 | domain_part: xn--tdali-d8a8w.lv 983 | domain: xn--tdali-d8a8w.lv 984 | ip: '' 985 | invalid: false 986 | invalid_reason: null 987 | - 988 | emails: testing@xn--tdali-d8a8w.lv 989 | multiple: true 990 | result: 991 | success: true 992 | reason: null 993 | email_addresses: 994 | - 995 | address: testing@xn--tdali-d8a8w.lv 996 | simple_address: testing@xn--tdali-d8a8w.lv 997 | original_address: testing@xn--tdali-d8a8w.lv 998 | name: '' 999 | name_parsed: '' 1000 | local_part: testing 1001 | local_part_parsed: testing 1002 | domain_part: xn--tdali-d8a8w.lv 1003 | domain: xn--tdali-d8a8w.lv 1004 | ip: '' 1005 | invalid: false 1006 | invalid_reason: null 1007 | - 1008 | emails: testing@xn--tdali-d8a8w.lv 1009 | multiple: false 1010 | result: 1011 | address: testing@xn--tdali-d8a8w.lv 1012 | simple_address: testing@xn--tdali-d8a8w.lv 1013 | original_address: testing@xn--tdali-d8a8w.lv 1014 | name: '' 1015 | name_parsed: '' 1016 | local_part: testing 1017 | local_part_parsed: testing 1018 | domain_part: xn--tdali-d8a8w.lv 1019 | domain: xn--tdali-d8a8w.lv 1020 | ip: '' 1021 | invalid: false 1022 | invalid_reason: null 1023 | - 1024 | emails: testing@-bad-domain.com 1025 | multiple: true 1026 | result: 1027 | success: false 1028 | reason: 'Invalid email address' 1029 | email_addresses: 1030 | - 1031 | address: '' 1032 | simple_address: '' 1033 | original_address: testing@-bad-domain.com 1034 | name: '' 1035 | name_parsed: '' 1036 | local_part: testing 1037 | local_part_parsed: testing 1038 | domain_part: '-bad-domain.com' 1039 | domain: '-bad-domain.com' 1040 | ip: '' 1041 | invalid: true 1042 | invalid_reason: 'Domain invalid: Parts of the domain name ''-bad-domain.com'' can not start or end with ''-''. This part does: -bad-domain' 1043 | - 1044 | emails: testing@-bad-domain.com 1045 | multiple: false 1046 | result: 1047 | address: '' 1048 | simple_address: '' 1049 | original_address: testing@-bad-domain.com 1050 | name: '' 1051 | name_parsed: '' 1052 | local_part: testing 1053 | local_part_parsed: testing 1054 | domain_part: '-bad-domain.com' 1055 | domain: '-bad-domain.com' 1056 | ip: '' 1057 | invalid: true 1058 | invalid_reason: 'Domain invalid: Parts of the domain name ''-bad-domain.com'' can not start or end with ''-''. This part does: -bad-domain' 1059 | - 1060 | emails: testing@192.168.0.1 1061 | multiple: true 1062 | result: 1063 | success: false 1064 | reason: 'Invalid email address' 1065 | email_addresses: 1066 | - 1067 | address: '' 1068 | simple_address: '' 1069 | original_address: testing@192.168.0.1 1070 | name: '' 1071 | name_parsed: '' 1072 | local_part: testing 1073 | local_part_parsed: testing 1074 | domain_part: '[192.168.0.1]' 1075 | domain: null 1076 | ip: 192.168.0.1 1077 | invalid: true 1078 | invalid_reason: 'IP address invalid (private): 192.168.0.1' 1079 | - 1080 | emails: testing@192.168.0.1 1081 | multiple: false 1082 | result: 1083 | address: '' 1084 | simple_address: '' 1085 | original_address: testing@192.168.0.1 1086 | name: '' 1087 | name_parsed: '' 1088 | local_part: testing 1089 | local_part_parsed: testing 1090 | domain_part: '[192.168.0.1]' 1091 | domain: null 1092 | ip: 192.168.0.1 1093 | invalid: true 1094 | invalid_reason: 'IP address invalid (private): 192.168.0.1' 1095 | - 1096 | emails: testing@256.26.52.5 1097 | multiple: true 1098 | result: 1099 | success: false 1100 | reason: 'Invalid email address' 1101 | email_addresses: 1102 | - 1103 | address: '' 1104 | simple_address: '' 1105 | original_address: testing@256.26.52.5 1106 | name: '' 1107 | name_parsed: '' 1108 | local_part: testing 1109 | local_part_parsed: testing 1110 | domain_part: '[256.26.52.5]' 1111 | domain: null 1112 | ip: 256.26.52.5 1113 | invalid: true 1114 | invalid_reason: 'IP address invalid: ''256.26.52.5'' does not appear to be a valid IP address' 1115 | - 1116 | emails: testing@256.26.52.5 1117 | multiple: false 1118 | result: 1119 | address: '' 1120 | simple_address: '' 1121 | original_address: testing@256.26.52.5 1122 | name: '' 1123 | name_parsed: '' 1124 | local_part: testing 1125 | local_part_parsed: testing 1126 | domain_part: '[256.26.52.5]' 1127 | domain: null 1128 | ip: 256.26.52.5 1129 | invalid: true 1130 | invalid_reason: 'IP address invalid: ''256.26.52.5'' does not appear to be a valid IP address' 1131 | - 1132 | emails: 'testing@[256.26.52.5]' 1133 | multiple: true 1134 | result: 1135 | success: false 1136 | reason: 'Invalid email address' 1137 | email_addresses: 1138 | - 1139 | address: '' 1140 | simple_address: '' 1141 | original_address: 'testing@[256.26.52.5]' 1142 | name: '' 1143 | name_parsed: '' 1144 | local_part: testing 1145 | local_part_parsed: testing 1146 | domain_part: '[256.26.52.5]' 1147 | domain: '' 1148 | ip: 256.26.52.5 1149 | invalid: true 1150 | invalid_reason: 'IP address invalid: ''256.26.52.5'' does not appear to be a valid IP address' 1151 | - 1152 | emails: 'testing@[256.26.52.5]' 1153 | multiple: false 1154 | result: 1155 | address: '' 1156 | simple_address: '' 1157 | original_address: 'testing@[256.26.52.5]' 1158 | name: '' 1159 | name_parsed: '' 1160 | local_part: testing 1161 | local_part_parsed: testing 1162 | domain_part: '[256.26.52.5]' 1163 | domain: '' 1164 | ip: 256.26.52.5 1165 | invalid: true 1166 | invalid_reason: 'IP address invalid: ''256.26.52.5'' does not appear to be a valid IP address' 1167 | - 1168 | emails: 'testing@[299.236.532.265]' 1169 | multiple: true 1170 | result: 1171 | success: false 1172 | reason: 'Invalid email address' 1173 | email_addresses: 1174 | - 1175 | address: '' 1176 | simple_address: '' 1177 | original_address: 'testing@[299.236.532.265]' 1178 | name: '' 1179 | name_parsed: '' 1180 | local_part: testing 1181 | local_part_parsed: testing 1182 | domain_part: '[299.236.532.265]' 1183 | domain: '' 1184 | ip: 299.236.532.265 1185 | invalid: true 1186 | invalid_reason: 'IP address invalid: ''299.236.532.265'' does not appear to be a valid IP address' 1187 | - 1188 | emails: 'testing@[299.236.532.265]' 1189 | multiple: false 1190 | result: 1191 | address: '' 1192 | simple_address: '' 1193 | original_address: 'testing@[299.236.532.265]' 1194 | name: '' 1195 | name_parsed: '' 1196 | local_part: testing 1197 | local_part_parsed: testing 1198 | domain_part: '[299.236.532.265]' 1199 | domain: '' 1200 | ip: 299.236.532.265 1201 | invalid: true 1202 | invalid_reason: 'IP address invalid: ''299.236.532.265'' does not appear to be a valid IP address' 1203 | - 1204 | emails: 'testing@[80.67.66.65]' 1205 | multiple: true 1206 | result: 1207 | success: true 1208 | reason: null 1209 | email_addresses: 1210 | - 1211 | address: 'testing@[80.67.66.65]' 1212 | simple_address: 'testing@[80.67.66.65]' 1213 | original_address: 'testing@[80.67.66.65]' 1214 | name: '' 1215 | name_parsed: '' 1216 | local_part: testing 1217 | local_part_parsed: testing 1218 | domain_part: '[80.67.66.65]' 1219 | domain: '' 1220 | ip: 80.67.66.65 1221 | invalid: false 1222 | invalid_reason: null 1223 | - 1224 | emails: 'testing@[80.67.66.65]' 1225 | multiple: false 1226 | result: 1227 | address: 'testing@[80.67.66.65]' 1228 | simple_address: 'testing@[80.67.66.65]' 1229 | original_address: 'testing@[80.67.66.65]' 1230 | name: '' 1231 | name_parsed: '' 1232 | local_part: testing 1233 | local_part_parsed: testing 1234 | domain_part: '[80.67.66.65]' 1235 | domain: '' 1236 | ip: 80.67.66.65 1237 | invalid: false 1238 | invalid_reason: null 1239 | - 1240 | emails: testing@80.67.66.65 1241 | multiple: true 1242 | result: 1243 | success: true 1244 | reason: null 1245 | email_addresses: 1246 | - 1247 | address: 'testing@[80.67.66.65]' 1248 | simple_address: 'testing@[80.67.66.65]' 1249 | original_address: testing@80.67.66.65 1250 | name: '' 1251 | name_parsed: '' 1252 | local_part: testing 1253 | local_part_parsed: testing 1254 | domain_part: '[80.67.66.65]' 1255 | domain: null 1256 | ip: 80.67.66.65 1257 | invalid: false 1258 | invalid_reason: null 1259 | - 1260 | emails: testing@80.67.66.65 1261 | multiple: false 1262 | result: 1263 | address: 'testing@[80.67.66.65]' 1264 | simple_address: 'testing@[80.67.66.65]' 1265 | original_address: testing@80.67.66.65 1266 | name: '' 1267 | name_parsed: '' 1268 | local_part: testing 1269 | local_part_parsed: testing 1270 | domain_part: '[80.67.66.65]' 1271 | domain: null 1272 | ip: 80.67.66.65 1273 | invalid: false 1274 | invalid_reason: null 1275 | - 1276 | emails: testing_underscore@somedomain.com 1277 | multiple: false 1278 | result: 1279 | address: 'testing_underscore@somedomain.com' 1280 | simple_address: 'testing_underscore@somedomain.com' 1281 | original_address: testing_underscore@somedomain.com 1282 | name: '' 1283 | name_parsed: '' 1284 | local_part: testing_underscore 1285 | local_part_parsed: testing_underscore 1286 | domain_part: somedomain.com 1287 | domain: somedomain.com 1288 | ip: '' 1289 | invalid: false 1290 | invalid_reason: null 1291 | - 1292 | emails: '"Supports (E-mail)" ' 1293 | multiple: false 1294 | result: 1295 | address: '"Supports (E-mail)" ' 1296 | simple_address: 'support@example.org' 1297 | original_address: '"Supports (E-mail)" ' 1298 | name: '"Supports (E-mail)"' 1299 | name_parsed: 'Supports (E-mail)' 1300 | local_part: 'support' 1301 | local_part_parsed: 'support' 1302 | domain_part: 'example.org' 1303 | domain: 'example.org' 1304 | ip: '' 1305 | invalid: false 1306 | invalid_reason: ~ 1307 | - 1308 | emails: 'Étienne Cloître ' 1309 | multiple: false 1310 | result: 1311 | address: 'Étienne Cloître ' 1312 | simple_address: 'e.cloitre@domain.tld' 1313 | original_address: 'Étienne Cloître ' 1314 | name: 'Étienne Cloître' 1315 | name_parsed: 'Étienne Cloître' 1316 | local_part: 'e.cloitre' 1317 | local_part_parsed: 'e.cloitre' 1318 | domain_part: 'domain.tld' 1319 | domain: 'domain.tld' 1320 | ip: '' 1321 | invalid: false 1322 | invalid_reason: ~ 1323 | - 1324 | emails: 'Étienne Cloître <é.cloître@domain.tld>' 1325 | multiple: false 1326 | result: 1327 | address: '' 1328 | simple_address: '' 1329 | original_address: 'Étienne Cloître <é.cloître@domain.tld>' 1330 | name: 'Étienne Cloître' 1331 | name_parsed: 'Étienne Cloître' 1332 | local_part: '' 1333 | local_part_parsed: '' 1334 | domain_part: '' 1335 | domain: '' 1336 | ip: '' 1337 | invalid: true 1338 | invalid_reason: "Invalid character found in email address (please put in quotes if needed): 'é'" 1339 | - 1340 | emails: 'é.cloître@domain.tld' 1341 | multiple: false 1342 | result: 1343 | address: '' 1344 | simple_address: '' 1345 | original_address: 'é.cloître@domain.tld' 1346 | name: '' 1347 | name_parsed: '' 1348 | local_part: '' 1349 | local_part_parsed: '' 1350 | domain_part: '' 1351 | domain: '' 1352 | ip: '' 1353 | invalid: true 1354 | invalid_reason: "Invalid character found in email address local part: 'î'" 1355 | - 1356 | emails: 'bob@i18ène.fr' 1357 | multiple: false 1358 | result: 1359 | address: 'bob@xn--i18ne-6ra.fr' 1360 | simple_address: 'bob@xn--i18ne-6ra.fr' 1361 | original_address: 'bob@i18ène.fr' 1362 | name: '' 1363 | name_parsed: '' 1364 | local_part: 'bob' 1365 | local_part_parsed: 'bob' 1366 | domain_part: 'xn--i18ne-6ra.fr' 1367 | domain: 'xn--i18ne-6ra.fr' 1368 | ip: '' 1369 | invalid: false 1370 | invalid_reason: ~ 1371 | - 1372 | emails: "I'm Bobé " 1373 | multiple: false 1374 | result: 1375 | address: "I'm Bobé " 1376 | simple_address: 'bob@xn--i18ne-6ra.fr' 1377 | original_address: "I'm Bobé " 1378 | name: "I'm Bobé" 1379 | name_parsed: "I'm Bobé" 1380 | local_part: 'bob' 1381 | local_part_parsed: 'bob' 1382 | domain_part: 'xn--i18ne-6ra.fr' 1383 | domain: 'xn--i18ne-6ra.fr' 1384 | ip: '' 1385 | invalid: false 1386 | invalid_reason: ~ 1387 | --------------------------------------------------------------------------------