Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/LengthLimits.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Email;

/**
* Email address length limits configuration
*
* Contains the maximum length constraints for email addresses
* as defined by RFC 5321, RFC 1035, and RFC erratum 1690
*/
class LengthLimits
{
private int $maxLocalPartLength;
private int $maxTotalLength;
private int $maxDomainLabelLength;

/**
* @param int $maxLocalPartLength Maximum length for local part (before @) in octets. Default: 64 per RFC 5321
* @param int $maxTotalLength Maximum total email length in octets. Default: 254 per RFC erratum 1690
* @param int $maxDomainLabelLength Maximum length for domain labels in characters. Default: 63 per RFC 1035
*/
public function __construct(
int $maxLocalPartLength = 64,
int $maxTotalLength = 254,
int $maxDomainLabelLength = 63
) {
$this->maxLocalPartLength = $maxLocalPartLength;
$this->maxTotalLength = $maxTotalLength;
$this->maxDomainLabelLength = $maxDomainLabelLength;
}

public function getMaxLocalPartLength(): int
{
return $this->maxLocalPartLength;
}

public function setMaxLocalPartLength(int $maxLocalPartLength): void
{
$this->maxLocalPartLength = $maxLocalPartLength;
}

public function getMaxTotalLength(): int
{
return $this->maxTotalLength;
}

public function setMaxTotalLength(int $maxTotalLength): void
{
$this->maxTotalLength = $maxTotalLength;
}

public function getMaxDomainLabelLength(): int
{
return $this->maxDomainLabelLength;
}

public function setMaxDomainLabelLength(int $maxDomainLabelLength): void
{
$this->maxDomainLabelLength = $maxDomainLabelLength;
}

/**
* Create LengthLimits with RFC-compliant defaults
*/
public static function createDefault(): self
{
return new self();
}

/**
* Create LengthLimits with relaxed constraints for legacy systems
*/
public static function createRelaxed(): self
{
return new self(128, 512, 128);
}
}
10 changes: 5 additions & 5 deletions src/Parse.php
Original file line number Diff line number Diff line change
Expand Up @@ -825,12 +825,12 @@ private function addAddress(
if (0 == mb_strlen($domainPart, $encoding)) {
$emailAddress['invalid'] = true;
$emailAddress['invalid_reason'] = 'Email address needs a domain after the \'@\'';
} elseif (mb_strlen($localPart, $encoding) > 63) {
} elseif (strlen($localPart) > $this->options->getMaxLocalPartLength()) {
$emailAddress['invalid'] = true;
$emailAddress['invalid_reason'] = 'Email address before the \'@\' can not be greater than 63 characters';
} elseif ((mb_strlen($localPart, $encoding) + mb_strlen($domainPart, $encoding) + 1) > 254) {
$emailAddress['invalid_reason'] = 'Email address before the \'@\' can not be greater than ' . $this->options->getMaxLocalPartLength() . ' octets per RFC 5321';
} elseif ((strlen($localPart) + strlen($domainPart) + 1) > $this->options->getMaxTotalLength()) {
$emailAddress['invalid'] = true;
$emailAddress['invalid_reason'] = 'Email addresses can not be greater than 254 characters';
$emailAddress['invalid_reason'] = 'Email addresses can not be greater than ' . $this->options->getMaxTotalLength() . ' octets per RFC erratum 1690';
}
}

Expand Down Expand Up @@ -879,7 +879,7 @@ protected function validateDomainName(string $domain, string $encoding = 'UTF-8'
$parts = mb_split('\\.', $domain);
mb_regex_encoding($origEncoding);
foreach ($parts as $part) {
if (mb_strlen($part, $encoding) > 63) {
if (mb_strlen($part, $encoding) > $this->options->getMaxDomainLabelLength()) {
return ['valid' => false, 'reason' => "Domain name part '{$part}' too long"];
}
if (!preg_match('/^[a-zA-Z0-9\-]+$/', $part)) {
Expand Down
52 changes: 50 additions & 2 deletions src/ParseOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@ class ParseOptions
/** @var array<string, bool> */
private array $separators = [];
private bool $useWhitespaceAsSeparator = true;
private LengthLimits $lengthLimits;

/**
* @param array<string> $bannedChars
* @param array<string> $separators
* @param bool $useWhitespaceAsSeparator
* @param LengthLimits|null $lengthLimits Email length limits. Uses RFC defaults if not provided
*/
public function __construct(array $bannedChars = [], array $separators = [','], bool $useWhitespaceAsSeparator = true)
{
public function __construct(
array $bannedChars = [],
array $separators = [','],
bool $useWhitespaceAsSeparator = true,
?LengthLimits $lengthLimits = null
) {
if ($bannedChars) {
$this->setBannedChars($bannedChars);
}
$this->setSeparators($separators);
$this->useWhitespaceAsSeparator = $useWhitespaceAsSeparator;
$this->lengthLimits = $lengthLimits ?? LengthLimits::createDefault();
}

/**
Expand Down Expand Up @@ -71,4 +78,45 @@ public function getUseWhitespaceAsSeparator(): bool
{
return $this->useWhitespaceAsSeparator;
}

public function setLengthLimits(LengthLimits $lengthLimits): void
{
$this->lengthLimits = $lengthLimits;
}

public function getLengthLimits(): LengthLimits
{
return $this->lengthLimits;
}

// Convenience methods for backward compatibility
public function setMaxLocalPartLength(int $maxLocalPartLength): void
{
$this->lengthLimits->setMaxLocalPartLength($maxLocalPartLength);
}

public function getMaxLocalPartLength(): int
{
return $this->lengthLimits->getMaxLocalPartLength();
}

public function setMaxTotalLength(int $maxTotalLength): void
{
$this->lengthLimits->setMaxTotalLength($maxTotalLength);
}

public function getMaxTotalLength(): int
{
return $this->lengthLimits->getMaxTotalLength();
}

public function setMaxDomainLabelLength(int $maxDomainLabelLength): void
{
$this->lengthLimits->setMaxDomainLabelLength($maxDomainLabelLength);
}

public function getMaxDomainLabelLength(): int
{
return $this->lengthLimits->getMaxDomainLabelLength();
}
}
21 changes: 19 additions & 2 deletions tests/ParseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,25 @@ public function testParseEmailAddresses()
// Check if test specifies custom separators
$separators = $test['separators'] ?? [',', ';'];

// Configure Parse to support configured separators
$options = new ParseOptions(['%', '!'], $separators, $useWhitespaceAsSeparator);
// Check if test specifies custom length limits
$lengthLimits = null;
if (isset($test['use_relaxed_limits']) && $test['use_relaxed_limits']) {
$lengthLimits = \Email\LengthLimits::createRelaxed();
} elseif (isset($test['max_local_part_length']) || isset($test['max_total_length']) || isset($test['max_domain_label_length'])) {
$lengthLimits = new \Email\LengthLimits(
$test['max_local_part_length'] ?? 64,
$test['max_total_length'] ?? 254,
$test['max_domain_label_length'] ?? 63
);
}

// Configure Parse to support configured separators and length limits
$options = new ParseOptions(
['%', '!'],
$separators,
$useWhitespaceAsSeparator,
$lengthLimits
);
$parser = new Parse(null, $options);

$this->assertSame($result, $parser->parse($emails, $multiple));
Expand Down
166 changes: 166 additions & 0 deletions tests/testspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1913,3 +1913,169 @@
ip: ''
invalid: false
invalid_reason: null
-
emails: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
multiple: false
result:
address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
simple_address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
original_address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
name: ''
name_parsed: ''
local_part: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
local_part_parsed: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
domain_part: example.com
domain: example.com
ip: ''
invalid: false
invalid_reason: null
-
emails: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
multiple: false
result:
address: ''
simple_address: ''
original_address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
name: ''
name_parsed: ''
local_part: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
local_part_parsed: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
domain_part: example.com
domain: example.com
ip: ''
invalid: true
invalid_reason: 'Email address before the ''@'' can not be greater than 64 octets per RFC 5321'
-
emails: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com'
multiple: false
result:
address: ''
simple_address: ''
original_address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com'
name: ''
name_parsed: ''
local_part: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
local_part_parsed: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
domain_part: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com'
domain: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com'
ip: ''
invalid: true
invalid_reason: 'Domain invalid: Domain name part ''aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'' too long'
-
emails: 'test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com'
multiple: false
result:
address: ''
simple_address: ''
original_address: 'test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com'
name: ''
name_parsed: ''
local_part: test
local_part_parsed: test
domain_part: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com
domain: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com
ip: ''
invalid: true
invalid_reason: 'Domain invalid: Domain name part ''aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'' too long'
-
emails: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
multiple: false
max_local_part_length: 70
result:
address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
simple_address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
original_address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
name: ''
name_parsed: ''
local_part: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
local_part_parsed: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
domain_part: example.com
domain: example.com
ip: ''
invalid: false
invalid_reason: null
-
emails: 'test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com'
multiple: false
max_domain_label_length: 70
result:
address: 'test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com'
simple_address: 'test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com'
original_address: 'test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com'
name: ''
name_parsed: ''
local_part: test
local_part_parsed: test
domain_part: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com
domain: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.example.com
ip: ''
invalid: false
invalid_reason: null
-
emails: '[email protected]'
multiple: false
max_local_part_length: 10
result:
address: ''
simple_address: ''
original_address: '[email protected]'
name: ''
name_parsed: ''
local_part: aaaaaaaaaaaa
local_part_parsed: aaaaaaaaaaaa
domain_part: example.com
domain: example.com
ip: ''
invalid: true
invalid_reason: 'Email address before the ''@'' can not be greater than 10 octets per RFC 5321'
-
emails: '[email protected]'
multiple: false
max_domain_label_length: 8
result:
address: ''
simple_address: ''
original_address: '[email protected]'
name: ''
name_parsed: ''
local_part: test
local_part_parsed: test
domain_part: aaaaaaaaa.example.com
domain: aaaaaaaaa.example.com
ip: ''
invalid: true
invalid_reason: 'Domain invalid: Domain name part ''aaaaaaaaa'' too long'
-
emails: '[email protected]'
multiple: false
max_total_length: 10
result:
address: ''
simple_address: ''
original_address: '[email protected]'
name: ''
name_parsed: ''
local_part: test
local_part_parsed: test
domain_part: example.com
domain: example.com
ip: ''
invalid: true
invalid_reason: 'Email addresses can not be greater than 10 octets per RFC erratum 1690'
-
emails: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
multiple: false
use_relaxed_limits: true
result:
address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
simple_address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
original_address: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com'
name: ''
name_parsed: ''
local_part: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
local_part_parsed: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
domain_part: example.com
domain: example.com
ip: ''
invalid: false
invalid_reason: null