diff options
| author | David Flatz | 2022-07-29 14:18:56 +0200 | 
|---|---|---|
| committer | David Flatz | 2022-07-29 14:51:07 +0200 | 
| commit | 744db7c9e5fc89d227c1beb981d01898ce478e41 (patch) | |
| tree | 64f1fcb4fb2a355e6fa3194008118301383bb7f1 | |
| parent | 4640a1e8b286c3ff2785abaddb9e449b023c88a7 (diff) | |
| download | itools-744db7c9e5fc89d227c1beb981d01898ce478e41.tar.gz itools-744db7c9e5fc89d227c1beb981d01898ce478e41.tar.bz2 itools-744db7c9e5fc89d227c1beb981d01898ce478e41.zip | |
improve validation and escaping of e-mail addresses: use same method for validation in check_email and address_error; encode and validate IDNs; make (more) robust functions to split address lists and emails
| -rw-r--r-- | it_mail.class | 150 | ||||
| -rwxr-xr-x | test/it_mail.t | 23 | 
2 files changed, 140 insertions, 33 deletions
| diff --git a/it_mail.class b/it_mail.class index 4119994..94801a5 100644 --- a/it_mail.class +++ b/it_mail.class @@ -301,6 +301,70 @@ function send($p = array())  /** + * INTERNAL: Split address lists in headers like From, To, Cc, Bcc into + * name/email pairs + * @param $string String containing address list + * @return Generator returning name/email tuples + */ +static function addrlist_split($string) +{ +	$quoted = false; +	foreach (str_split($string) as $char) +	{ +		if ($char == '"') +			$quoted = !$quoted; + +		if ($char == ',' && !$quoted) +			$n++; +		else +			$mailboxes[$n] .= $char; +	} + +	foreach ((array)$mailboxes as $mailbox) +	{ +		$mode = 'name'; +		$name = $email = ''; +		$quoted = false; +		foreach (str_split($mailbox) as $char) +		{ +			if ($char == '"') +				$quoted = !$quoted; +			switch ($mode) +			{ +			case 'name': +				if ($char == '<' && !$quoted) +					$mode = 'email'; +				else +					$name .= $char; +				break; +			case 'email': +				if ($char == '>' && !$quoted) +				{ +					$mode = 'finish'; +					break 2; +				} +				else +					$email .= $char; +				break; +			} +		} +		if ($mode == 'email') +		{ +			$name = $name . '<' . $email; +			$email = ''; +			$mode = 'name'; +		} +		if ($mode == 'name' && !$email) +		{ +			$email = it::replace(['^"(.*)"$' => '$1'], trim($name)); +			$name = ''; +		} +		yield [$name, trim($email)]; +	} +} + + +/**   * INTERNAL: Escape header line with ?=?<charset>?Q if necessary, e.g. in Subject line   * @param $string String to be escaped   * @return String escape suitable for sending in header line @@ -328,23 +392,13 @@ function addrlist_escape($string)  {  	# Exclude e-mail addresses from being encoded as  	# e.g. GMail or Exchange have problems with that -	foreach (str_split($string) as $char) +	foreach (self::addrlist_split($string) as list($name, $email))  	{ -		if ($char == '"') -			$quoted = !$quoted; - -		if ($char == ',' && !$quoted) -			$n++; -		else -			$mailboxes[$n] .= $char; -	} - -	foreach ((array)$mailboxes as $mailbox) -	{ -		if (preg_match('/^(.*)(\s+?<[^>]+@[^>]+>\s*)$/', $mailbox, $matches)) -			$result[] = $this->header_escape(trim($matches[1])) . $matches[2]; -		else -			$result[] = trim($mailbox); +		$email = self::email_escape($email); +		if ($name && $email) +			$result[] = $this->header_escape(trim($name)) . " <$email>"; +		else if (!$name && $email) +			$result[] = $email;  	}  	return implode(', ', $result); @@ -364,6 +418,48 @@ static function fullname_escape($string)  /** + * INTERNAL: Split email-address into local part and doomain + * @return Array with to elements: local part and domain + */ +static function email_split($email) +{ +	$mode = 'local'; +	$local = $domain = ''; +	$quoted = false; +	foreach (str_split($email) as $char) +	{ +		if ($char == '"') +			$quoted = !$quoted; +		switch ($mode) +		{ +		case 'local': +			if ($char == '@' && !$quoted) +				$mode = 'domain'; +			else +				$local .= $char; +			break; +		case 'domain': +			$domain .= $char; +		} +	} + +	return [$local, $domain]; +} + + +/** + * INTERNAL: Convert domain part of email address into ascii idn form + * @return String with converted email address + */ +static function email_escape($email) +{ +	list($local, $domain) = self::email_split($email); + +	return $local . ($domain ? '@' . idn_to_ascii($domain) : ''); +} + + +/**   * INTERNAL: Send SMTP command and return true if result is 2XX   * @param $fp Connection to SMTP server opened using fsockopen   * @param $command Command to send, e.g. HELO gna.ch @@ -408,20 +504,12 @@ static function send_smtp_cmd($fp, $cmd, &$answer, $timeoutok = false, $failcode   */  static function address_error($addresslist)  { -	foreach (str_split($addresslist) as $char) +	foreach (self::addrlist_split($addresslist) as list($dummy, $email))  	{ -		if ($char == '"') -			$quoted = !$quoted; -		else if ($char == ',' && !$quoted) -			$n++; -		else if (!$quoted) -			$addresses[$n] .= $char; +		$email = self::email_escape($email); +		if (filter_var($email, FILTER_VALIDATE_EMAIL) === false && !it::match('^\s*[a-z][-a-z0-9]*\s*$', $email)) +			return "invalid format on $email";  	} - -	foreach ($addresses as $address) -		if (($email = it::match('<([^>]*)>', $address) ?? trim($address))) -			if (filter_var($email, FILTER_VALIDATE_EMAIL) === false && !it::match('^\s*[a-z][-a-z0-9]*\s*$', $email)) -				return "invalid format on $email";  } @@ -438,11 +526,11 @@ static function check_email($email, $checkmailbox = false)  {  	$result = IT_MAIL_CHECKEMAIL_INVALID; +	$email = self::email_escape($email); +	list($dummy, $domain) = self::email_split($email);  	/* Check if username starts with www. or not well-formed => reject */ -	if (!preg_match('/^www\./', $email) && preg_match('/^[a-z0-9&_+.-]+@([a-z0-9.-]+\.[a-z]+)$/i', $email, $regs)) +	if (!preg_match('/^www\./', $email) && filter_var($email, FILTER_VALIDATE_EMAIL) !== false && $domain)  	{ -		$domain = $regs[1]; -  		if (!getmxrr($domain, $mxhosts, $weights))  		{  			/* If we find no MX we check if domain exists */ diff --git a/test/it_mail.t b/test/it_mail.t index 7e892c2..6aefd84 100755 --- a/test/it_mail.t +++ b/test/it_mail.t @@ -56,6 +56,12 @@ is(  	"Don't remove quoting characters from realname when it contains a quotable character"  ); +is( +	$mail->addrlist_escape('"Alfred E. Neuman" <t€st@sör.ch>', true), +	'"Alfred E. Neuman" <t€st@xn--sr-fka.ch>', +	"Convert domain in email to idn version" +); +  $mail = new it_mail([  	'From' => 'Someone Ïmportant <ïmportant@search.ch>',  	'To' => 'éxample@example.com, example@example.com, Sömeone Ëlse <sömeone@example.com>, "Alfred E. Neuman" <neuman@example.com>, "Schmitt, Sören" <schmitt@example.com>', @@ -95,10 +101,23 @@ is(  );  is( -	it_mail::address_error('neuman@example.com, <neuman@example.com>, "Neuman, Alfred E." <neuman@example.com>, aneuman, "<neuman@example>" <neuman@example.com>', " neumann@example.com"), +	it_mail::address_error('neuman@example.com, <neuman@example.com>, "Neuman, Alfred E." <neuman@example.com>, aneuman, "<neuman@example>" <neuman@example.com>, " neumann@example.com", "www@search.ch".foo.bar@local.ch, test@sör.ch'),  	null,  	"Accept valid e-mail addresses"  ); -foreach (['neuman@example', '"neuman@example.com" <neuman@example>', '<neuma@example.com'] as $email) +foreach (['neuman@example', '"neuman@example.com" <neuman@example>', '<neuma@example.com', 'shaqroot.@yahoo.com'] as $email) +{  	isnt(it_mail::address_error($email), null, "Reject invalid e-mail address $email"); +	foreach (it_mail::addrlist_split($email) as list($dummy, $email)) +		is(it_mail::check_email($email), IT_MAIL_CHECKEMAIL_INVALID, "check_email rejects invalid email address $email too"); +} + +is(it_mail::check_email('test@sör.ch'), IT_MAIL_CHECKEMAIL_OK, "check_email accepts valid email address with idn domain"); + +is(it_mail::email_split('test@sör.ch'), ['test', 'sör.ch'], 'email_split test@sör.ch'); +is(it_mail::email_split('"test@local.ch".foo.bar@search.ch'), ['"test@local.ch".foo.bar', 'search.ch'], 'email_split "test@local.ch".foo.bar@search.ch'); +is(it_mail::email_split('aneuman'), ['aneuman', ''], 'email_split aneuman'); + +is(it_mail::email_escape('test@sör.ch'), 'test@xn--sr-fka.ch', 'idn encode domain sör.ch'); +is(it_mail::email_escape('أحمد@مثال.آزمایشی'), 'أحمد@xn--mgbh0fb.xn--hgbk6aj7f53bba', 'idn encode domain ﻢﺛﺎﻟ.ﺁﺰﻣﺎﯿﺸﯾ'); |