Skip to content
This repository has been archived by the owner on Nov 17, 2021. It is now read-only.

Basic IDN support #1044

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGES
@@ -1,10 +1,10 @@
Changelog
=========

6.0.3 (2017-XX-XX)
6.1.0 (2017-XX-XX)
------------------

* n/a
* added support for IDN domains in email addresses

6.0.2 (2017-09-30)
------------------
Expand Down
4 changes: 4 additions & 0 deletions composer.json
Expand Up @@ -22,6 +22,10 @@
"mockery/mockery": "~0.9.1",
"symfony/phpunit-bridge": "~3.3@dev"
},
"suggest": {
"ext-intl": "Needed to support internationalized email addresses",
"true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed"
},
"autoload": {
"files": ["lib/swift_required.php"]
},
Expand Down
13 changes: 13 additions & 0 deletions doc/headers.rst
Expand Up @@ -383,6 +383,19 @@ following::

*/

Internationalized domains are automatically converted to IDN encoding::

$to = $message->getHeaders()->get('To');
$to->setAddresses('joe@ëxämple.org');

echo $to->toString();

/*

To: joe@xn--xmple-gra1c.org

*/

ID Headers
~~~~~~~~~~

Expand Down
25 changes: 25 additions & 0 deletions lib/classes/Swift/AddressEncoder.php
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of SwiftMailer.
* (c) 2018 Christian Schmidt
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* Email address encoder.
*
* @author Christian Schmidt
*/
interface Swift_AddressEncoder
{
/**
* Encodes an email address.
*
* @throws Swift_AddressEncoderException If the email cannot be represented in
* the encoding implemented by this class.
*/
public function encodeString(string $address): string;
}
60 changes: 60 additions & 0 deletions lib/classes/Swift/AddressEncoder/IdnAddressEncoder.php
@@ -0,0 +1,60 @@
<?php

/*
* This file is part of SwiftMailer.
* (c) 2018 Christian Schmidt
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* An IDN email address encoder.
*
* @author Christian Schmidt
*/
class Swift_AddressEncoder_IdnAddressEncoder implements Swift_AddressEncoder
{
/**
* Encodes the domain part of an address using IDN.
*
* @throws Swift_AddressEncoderException If local-part contains non-ASCII characters
*/
public function encodeString(string $address): string
{
$i = strrpos($address, '@');
if (false !== $i) {
$local = substr($address, 0, $i);
$domain = substr($address, $i + 1);

if (preg_match('/[^\x00-\x7F]/', $local)) {
throw new Swift_AddressEncoderException('Non-ASCII characters not supported in local-part', $address);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The arguments probably need to be the other way round (or in the constructor)

}

$address = sprintf('%s@%s', $local, $this->idnToAscii($domain));
}

return $address;
}

/**
* IDN-encodes a UTF-8 string to ASCII.
*
* @param string $string
*
* @return string
*/
protected function idnToAscii(string $string): string
{
if (function_exists('idn_to_ascii')) {
return idn_to_ascii($string, 0, INTL_IDNA_VARIANT_UTS46);
}

if (class_exists('TrueBV\Punycode')) {
$punycode = new \TrueBV\Punycode();
return $punycode->encode($string);
}

throw new Swift_SwiftException('No IDN encoder found (install the intl extension or the true/punycode package');
}
}
35 changes: 35 additions & 0 deletions lib/classes/Swift/AddressEncoderException.php
@@ -0,0 +1,35 @@
<?php

/*
* This file is part of SwiftMailer.
* (c) 2018 Christian Schmidt
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* AddressEncoderException when the specified email address is in a format that
* cannot be encoded by a given address encoder.
*
* @author Christian Schmidt
*/
class Swift_AddressEncoderException extends Swift_RfcComplianceException
{
/** The address that could not be encoded */
protected $address;

public function __construct(string $message, string $address)
{
parent::__construct($message);
$this->address = $address;
}

/**
* Returns the address that could not be encoded.
*/
public function getAddress(): string
{
return $this->address;
}
}
12 changes: 8 additions & 4 deletions lib/classes/Swift/Mime/Headers/IdentificationHeader.php
Expand Up @@ -34,16 +34,20 @@ class Swift_Mime_Headers_IdentificationHeader extends Swift_Mime_Headers_Abstrac
*/
private $emailValidator;

private $addressEncoder;

/**
* Creates a new IdentificationHeader with the given $name and $id.
*
* @param string $name
* @param EmailValidator $emailValidator
* @param string $name
* @param emailValidator $emailValidator
* @param Swift_AddressEncoder $addressEncoder
*/
public function __construct($name, EmailValidator $emailValidator)
public function __construct($name, EmailValidator $emailValidator, Swift_AddressEncoder $addressEncoder = null)
{
$this->setFieldName($name);
$this->emailValidator = $emailValidator;
$this->addressEncoder = $addressEncoder ?? new Swift_AddressEncoder_IdnAddressEncoder();
}

/**
Expand Down Expand Up @@ -159,7 +163,7 @@ public function getFieldBody()
$angleAddrs = array();

foreach ($this->ids as $id) {
$angleAddrs[] = '<'.$id.'>';
$angleAddrs[] = '<'.$this->addressEncoder->encodeString($id).'>';
}

$this->setCachedValue(implode(' ', $angleAddrs));
Expand Down
8 changes: 6 additions & 2 deletions lib/classes/Swift/Mime/Headers/MailboxHeader.php
Expand Up @@ -32,18 +32,22 @@ class Swift_Mime_Headers_MailboxHeader extends Swift_Mime_Headers_AbstractHeader
*/
private $emailValidator;

private $addressEncoder;

/**
* Creates a new MailboxHeader with $name.
*
* @param string $name of Header
* @param Swift_Mime_HeaderEncoder $encoder
* @param EmailValidator $emailValidator
* @param Swift_AddressEncoder $addressEncoder
*/
public function __construct($name, Swift_Mime_HeaderEncoder $encoder, EmailValidator $emailValidator)
public function __construct($name, Swift_Mime_HeaderEncoder $encoder, EmailValidator $emailValidator, Swift_AddressEncoder $addressEncoder = null)
{
$this->setFieldName($name);
$this->setEncoder($encoder);
$this->emailValidator = $emailValidator;
$this->addressEncoder = $addressEncoder ?? new Swift_AddressEncoder_IdnAddressEncoder();
}

/**
Expand Down Expand Up @@ -330,7 +334,7 @@ private function createNameAddressStrings(array $mailboxes)
$strings = array();

foreach ($mailboxes as $email => $name) {
$mailboxStr = $email;
$mailboxStr = $this->addressEncoder->encodeString($email);
if (null !== $name) {
$nameStr = $this->createDisplayNameString($name, empty($strings));
$mailboxStr = $nameStr.' <'.$mailboxStr.'>';
Expand Down
13 changes: 9 additions & 4 deletions lib/classes/Swift/Mime/Headers/PathHeader.php
Expand Up @@ -32,16 +32,20 @@ class Swift_Mime_Headers_PathHeader extends Swift_Mime_Headers_AbstractHeader
*/
private $emailValidator;

private $addressEncoder;

/**
* Creates a new PathHeader with the given $name.
*
* @param string $name
* @param EmailValidator $emailValidator
* @param string $name
* @param EmailValidator $emailValidator
* @param Swift_AddressEncoder $addressEncoder
*/
public function __construct($name, EmailValidator $emailValidator)
public function __construct($name, EmailValidator $emailValidator, Swift_AddressEncoder $addressEncoder = null)
{
$this->setFieldName($name);
$this->emailValidator = $emailValidator;
$this->addressEncoder = $addressEncoder ?? new Swift_AddressEncoder_IdnAddressEncoder();
}

/**
Expand Down Expand Up @@ -127,7 +131,8 @@ public function getFieldBody()
{
if (!$this->getCachedValue()) {
if (isset($this->address)) {
$this->setCachedValue('<'.$this->address.'>');
$address = $this->addressEncoder->encodeString($this->address);
$this->setCachedValue('<'.$address.'>');
}
}

Expand Down
6 changes: 4 additions & 2 deletions lib/classes/Swift/Mime/SimpleHeaderFactory.php
Expand Up @@ -36,13 +36,15 @@ class Swift_Mime_SimpleHeaderFactory implements Swift_Mime_CharsetObserver
* @param Swift_Encoder $paramEncoder
* @param EmailValidator $emailValidator
* @param string|null $charset
* @param Swift_AddressEncoder $addressEncoder
*/
public function __construct(Swift_Mime_HeaderEncoder $encoder, Swift_Encoder $paramEncoder, EmailValidator $emailValidator, $charset = null)
public function __construct(Swift_Mime_HeaderEncoder $encoder, Swift_Encoder $paramEncoder, EmailValidator $emailValidator, $charset = null, Swift_AddressEncoder $addressEncoder = null)
{
$this->encoder = $encoder;
$this->paramEncoder = $paramEncoder;
$this->emailValidator = $emailValidator;
$this->charset = $charset;
$this->addressEncoder = $addressEncoder ?? new Swift_AddressEncoder_IdnAddressEncoder();
}

/**
Expand All @@ -55,7 +57,7 @@ public function __construct(Swift_Mime_HeaderEncoder $encoder, Swift_Encoder $pa
*/
public function createMailboxHeader($name, $addresses = null)
{
$header = new Swift_Mime_Headers_MailboxHeader($name, $this->encoder, $this->emailValidator);
$header = new Swift_Mime_Headers_MailboxHeader($name, $this->encoder, $this->emailValidator, $this->addressEncoder);
if (isset($addresses)) {
$header->setFieldBodyModel($addresses);
}
Expand Down
10 changes: 8 additions & 2 deletions lib/classes/Swift/Transport/AbstractSmtpTransport.php
Expand Up @@ -27,6 +27,8 @@ abstract class Swift_Transport_AbstractSmtpTransport implements Swift_Transport
/** The event dispatching layer */
protected $eventDispatcher;

protected $addressEncoder;

/** Source Ip */
protected $sourceIp;

Expand All @@ -39,11 +41,13 @@ abstract protected function getBufferParams();
* @param Swift_Transport_IoBuffer $buf
* @param Swift_Events_EventDispatcher $dispatcher
* @param string $localDomain
* @param Swift_AddressEncoder $addressEncoder
*/
public function __construct(Swift_Transport_IoBuffer $buf, Swift_Events_EventDispatcher $dispatcher, $localDomain = '127.0.0.1')
public function __construct(Swift_Transport_IoBuffer $buf, Swift_Events_EventDispatcher $dispatcher, $localDomain = '127.0.0.1', Swift_AddressEncoder $addressEncoder = null)
{
$this->eventDispatcher = $dispatcher;
$this->buffer = $buf;
$this->eventDispatcher = $dispatcher;
$this->addressEncoder = $addressEncoder ?? new Swift_AddressEncoder_IdnAddressEncoder();
$this->setLocalDomain($localDomain);
}

Expand Down Expand Up @@ -336,6 +340,7 @@ protected function doHeloCommand()
/** Send the MAIL FROM command */
protected function doMailFromCommand($address)
{
$address = $this->addressEncoder->encodeString($address);
$this->executeCommand(
sprintf("MAIL FROM:<%s>\r\n", $address), array(250)
);
Expand All @@ -344,6 +349,7 @@ protected function doMailFromCommand($address)
/** Send the RCPT TO command */
protected function doRcptToCommand($address)
{
$address = $this->addressEncoder->encodeString($address);
$this->executeCommand(
sprintf("RCPT TO:<%s>\r\n", $address), array(250, 251, 252)
);
Expand Down
7 changes: 5 additions & 2 deletions lib/classes/Swift/Transport/EsmtpTransport.php
Expand Up @@ -52,10 +52,11 @@ class Swift_Transport_EsmtpTransport extends Swift_Transport_AbstractSmtpTranspo
* @param Swift_Transport_EsmtpHandler[] $extensionHandlers
* @param Swift_Events_EventDispatcher $dispatcher
* @param string $localDomain
* @param Swift_AddressEncoder $addressEncoder
*/
public function __construct(Swift_Transport_IoBuffer $buf, array $extensionHandlers, Swift_Events_EventDispatcher $dispatcher, $localDomain = '127.0.0.1')
public function __construct(Swift_Transport_IoBuffer $buf, array $extensionHandlers, Swift_Events_EventDispatcher $dispatcher, $localDomain = '127.0.0.1', Swift_AddressEncoder $addressEncoder = null)
{
parent::__construct($buf, $dispatcher, $localDomain);
parent::__construct($buf, $dispatcher, $localDomain, $addressEncoder);
$this->setExtensionHandlers($extensionHandlers);
}

Expand Down Expand Up @@ -338,6 +339,7 @@ protected function doHeloCommand()
/** Overridden to add Extension support */
protected function doMailFromCommand($address)
{
$address = $this->addressEncoder->encodeString($address);
$handlers = $this->getActiveHandlers();
$params = array();
foreach ($handlers as $handler) {
Expand All @@ -352,6 +354,7 @@ protected function doMailFromCommand($address)
/** Overridden to add Extension support */
protected function doRcptToCommand($address)
{
$address = $this->addressEncoder->encodeString($address);
$handlers = $this->getActiveHandlers();
$params = array();
foreach ($handlers as $handler) {
Expand Down
4 changes: 4 additions & 0 deletions lib/dependency_maps/mime_deps.php
Expand Up @@ -68,6 +68,7 @@
'mime.rfc2231encoder',
'email.validator',
'properties.charset',
'mime.addressencoder',
))

->register('mime.headerset')
Expand Down Expand Up @@ -125,6 +126,9 @@
->register('mime.rfc2231encoder')
->asNewInstanceOf('Swift_Encoder_Rfc2231Encoder')
->withDependencies(array('mime.charstream'))

->register('mime.addressencoder')
->asNewInstanceOf('Swift_AddressEncoder_IdnAddressEncoder')
;

unset($swift_mime_types);
4 changes: 4 additions & 0 deletions lib/dependency_maps/transport_deps.php
Expand Up @@ -14,6 +14,7 @@
array('transport.authhandler'),
'transport.eventdispatcher',
'transport.localdomain',
'transport.addressencoder',
))

->register('transport.sendmail')
Expand Down Expand Up @@ -72,6 +73,9 @@
->register('transport.eventdispatcher')
->asNewInstanceOf('Swift_Events_SimpleEventDispatcher')

->register('transport.addressencoder')
->asNewInstanceOf('Swift_AddressEncoder_IdnAddressEncoder')

->register('transport.replacementfactory')
->asSharedInstanceOf('Swift_StreamFilters_StringReplacementFilterFactory')
;