[TASK] IDNA encode domains in email addresses for MailMessage 94/43294/5
authorMorton Jonuschat <m.jonuschat@mojocode.de>
Mon, 14 Sep 2015 11:40:41 +0000 (13:40 +0200)
committerChristian Kuhn <lolli@schwarzbu.ch>
Tue, 15 Sep 2015 09:29:48 +0000 (11:29 +0200)
SwiftMailer does not support email addresses with UTF8 characters in the
domain name. UTF8 characters in the domain name are valid as long as
they are IDNA/punycode encoded.

To work around the SwiftMailer issue MailMessage has been extended with
custom setters that encode the email addresses before passing them to
SwiftMailer.

Resolves: #69208
Releases: master
Change-Id: I9835340c8d216f37dc94dfb65dc70822c2a47702
Reviewed-on: http://review.typo3.org/43294
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Nicole Cordes <typo3@cordes.co>
Tested-by: Nicole Cordes <typo3@cordes.co>
Reviewed-by: Loek Hilgersom <loek@netcoop.nl>
Tested-by: Loek Hilgersom <loek@netcoop.nl>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
typo3/sysext/core/Classes/Mail/MailMessage.php
typo3/sysext/core/Tests/Unit/Mail/MailMessageTest.php [new file with mode: 0644]

index 527040a..79e4943 100644 (file)
@@ -80,4 +80,171 @@ class MailMessage extends \Swift_Message {
                return $this->failedRecipients;
        }
 
+       /**
+        * Set the return-path (the bounce address) of this message.
+        *
+        * @param string $address
+        * @return \TYPO3\CMS\Core\Mail\MailMessage
+        */
+       public function setReturnPath($address) {
+               $address = $this->idnaEncodeAddresses($address);
+               return parent::setReturnPath($address);
+       }
+
+       /**
+        * Set the sender of this message.
+        *
+        * This does not override the From field, but it has a higher significance.
+        *
+        * @param string $address
+        * @param string $name optional
+        * @return \TYPO3\CMS\Core\Mail\MailMessage
+        */
+       public function setSender($address, $name = NULL) {
+               $address = $this->idnaEncodeAddresses($address);
+               return parent::setSender($address, $name);
+       }
+
+       /**
+        * Set the from address of this message.
+        *
+        * You may pass an array of addresses if this message is from multiple people.
+        *
+        * If $name is passed and the first parameter is a string, this name will be
+        * associated with the address.
+        *
+        * @param string|array $addresses
+        * @param string $name optional
+        * @return \TYPO3\CMS\Core\Mail\MailMessage
+        */
+       public function setFrom($addresses, $name = NULL) {
+               $addresses = $this->idnaEncodeAddresses($addresses);
+               return parent::setFrom($addresses, $name);
+       }
+
+       /**
+        * Set the reply-to address of this message.
+        *
+        * You may pass an array of addresses if replies will go to multiple people.
+        *
+        * If $name is passed and the first parameter is a string, this name will be
+        * associated with the address.
+        *
+        * @param string|array $addresses
+        * @param string $name optional
+        * @return \TYPO3\CMS\Core\Mail\MailMessage
+        */
+       public function setReplyTo($addresses, $name = NULL) {
+               $addresses = $this->idnaEncodeAddresses($addresses);
+               return parent::setReplyTo($addresses, $name);
+       }
+
+       /**
+        * Set the to addresses of this message.
+        *
+        * If multiple recipients will receive the message an array should be used.
+        * Example: array('receiver@domain.org', 'other@domain.org' => 'A name')
+        *
+        * If $name is passed and the first parameter is a string, this name will be
+        * associated with the address.
+        *
+        * @param string|array $addresses
+        * @param string $name optional
+        * @return \TYPO3\CMS\Core\Mail\MailMessage
+        */
+       public function setTo($addresses, $name = NULL) {
+               $addresses = $this->idnaEncodeAddresses($addresses);
+               return parent::setTo($addresses, $name);
+       }
+
+       /**
+        * Set the Cc addresses of this message.
+        *
+        * If $name is passed and the first parameter is a string, this name will be
+        * associated with the address.
+        *
+        * @param string|array $addresses
+        * @param string $name optional
+        * @return \TYPO3\CMS\Core\Mail\MailMessage
+        */
+       public function setCc($addresses, $name = NULL) {
+               $addresses = $this->idnaEncodeAddresses($addresses);
+               return parent::setCc($addresses, $name);
+       }
+
+       /**
+        * Set the Bcc addresses of this message.
+        *
+        * If $name is passed and the first parameter is a string, this name will be
+        * associated with the address.
+        *
+        * @param string|array $addresses
+        * @param string $name optional
+        * @return \TYPO3\CMS\Core\Mail\MailMessage
+        */
+       public function setBcc($addresses, $name = NULL) {
+               $addresses = $this->idnaEncodeAddresses($addresses);
+               return parent::setBcc($addresses, $name);
+       }
+
+       /**
+        * Ask for a delivery receipt from the recipient to be sent to $addresses.
+        *
+        * @param array $addresses
+        * @return \TYPO3\CMS\Core\Mail\MailMessage
+        */
+       public function setReadReceiptTo($addresses) {
+               $addresses = $this->idnaEncodeAddresses($addresses);
+               return parent::setReadReceiptTo($addresses);
+       }
+
+       /**
+        * IDNA encode email addresses. Accepts addresses in all formats that SwiftMailer supports
+        *
+        * @param string|array $addresses
+        * @return string|array
+        */
+       protected function idnaEncodeAddresses($addresses) {
+               if (!is_array($addresses)) {
+                       return $this->idnaEncodeAddress($addresses);
+               }
+               $newAddresses = [];
+               foreach ($addresses as $email => $name) {
+                       if (ctype_digit($email)) {
+                               $newAddresses[] = $this->idnaEncodeAddress($name);
+                       } else {
+                               $newAddresses[$this->idnaEncodeAddress($email)] = $name;
+                       }
+               }
+
+               return $newAddresses;
+       }
+
+       /**
+        * IDNA encode the domain part of an email address if it contains non ASCII characters
+        *
+        * @param mixed $email
+        * @return mixed
+        * @see \TYPO3\CMS\Core\Utility\GeneralUtility::validEmail
+        */
+       protected function idnaEncodeAddress($email) {
+               // Early return in case input is not a string
+               if (!is_string($email)) {
+                       return $email;
+               }
+               // Split on the last "@" since adresses like "foo@bar"@example.org are valid
+               $atPosition = strrpos($email, '@');
+               if (!$atPosition || $atPosition + 1 === strlen($email)) {
+                       // Return if no @ found or it is placed at the very beginning or end of the email
+                       return $email;
+               }
+               $domain = substr($email, $atPosition + 1);
+               $local = substr($email, 0, $atPosition);
+               if (!mb_check_encoding($domain, 'ASCII')) {
+                       $domain = \TYPO3\CMS\Core\Utility\GeneralUtility::idnaEncode($domain);
+               }
+
+               return $local . '@' . $domain;
+       }
+
 }
diff --git a/typo3/sysext/core/Tests/Unit/Mail/MailMessageTest.php b/typo3/sysext/core/Tests/Unit/Mail/MailMessageTest.php
new file mode 100644 (file)
index 0000000..65c145b
--- /dev/null
@@ -0,0 +1,213 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Mail;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Testcase for the TYPO3\CMS\Core\Mail\MailMessage class.
+ */
+class MailMessageTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var \TYPO3\CMS\Core\Mail\MailMessage
+        */
+       protected $subject;
+
+       protected function setUp() {
+               $this->subject = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
+       }
+
+       /**
+        * @returns array
+        */
+       public function returnPathEmailAddressDataProvider() {
+               return [
+                       'string with ascii email address' => [
+                               'john.doe@example.com',
+                               'john.doe@example.com'
+                       ],
+                       'string with utf8 email address' => [
+                               'john.doe@☺example.com',
+                               'john.doe@xn--example-tc7d.com'
+                       ]
+               ];
+       }
+
+       /**
+        * @test
+        * @param string $address
+        * @param string $expected
+        * @dataProvider returnPathEmailAddressDataProvider
+        */
+       public function setReturnPathIdnaEncodesAddresses($address, $expected) {
+               $this->subject->setReturnPath($address);
+
+               $this->assertSame($expected, $this->subject->getReturnPath());
+       }
+
+       /**
+        * @returns array
+        */
+       public function senderEmailAddressDataProvider() {
+               return [
+                       'string with ascii email address' => [
+                               'john.doe@example.com',
+                               [
+                                       'john.doe@example.com' => NULL,
+                               ]
+                       ],
+                       'string with utf8 email address' => [
+                               'john.doe@☺example.com',
+                               [
+                                       'john.doe@xn--example-tc7d.com' => NULL,
+                               ]
+                       ]
+               ];
+       }
+
+       /**
+        * @test
+        * @param string $address
+        * @param array $expected
+        * @dataProvider senderEmailAddressDataProvider
+        */
+       public function setSenderIdnaEncodesAddresses($address, $expected) {
+               $this->subject->setSender($address);
+
+               $this->assertSame($expected, $this->subject->getSender());
+       }
+
+       /**
+        * @returns array
+        */
+       public function emailAddressesDataProvider() {
+               return [
+                       'string with ascii email address' => [
+                               'john.doe@example.com',
+                               [
+                                       'john.doe@example.com' => NULL
+                               ]
+                       ],
+                       'string with utf8 email address' => [
+                               'john.doe@☺example.com',
+                               [
+                                       'john.doe@xn--example-tc7d.com' => NULL
+                               ]
+                       ],
+                       'array with ascii email addresses' => [
+                               [
+                                       'john.doe@example.com' => 'John Doe',
+                                       'jane.doe@example.com'
+                               ],
+                               [
+                                       'john.doe@example.com' => 'John Doe',
+                                       'jane.doe@example.com' => NULL,
+                               ],
+                       ],
+                       'array with utf8 email addresses' => [
+                               [
+                                       'john.doe@☺example.com' => 'John Doe',
+                                       'jane.doe@äöu.com' => 'Jane Doe',
+                               ],
+                               [
+                                       'john.doe@xn--example-tc7d.com' => 'John Doe',
+                                       'jane.doe@xn--u-zfa8c.com' => 'Jane Doe',
+                               ],
+                       ],
+                       'array with mixed email addresses' => [
+                               [
+                                       'john.doe@☺example.com' => 'John Doe',
+                                       'jane.doe@example.com' => 'Jane Doe',
+                               ],
+                               [
+                                       'john.doe@xn--example-tc7d.com' => 'John Doe',
+                                       'jane.doe@example.com' => 'Jane Doe',
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @test
+        * @param string|array $addresses
+        * @param string|array $expected
+        * @dataProvider emailAddressesDataProvider
+        */
+       public function setFromIdnaEncodesAddresses($addresses, $expected) {
+               $this->subject->setFrom($addresses);
+
+               $this->assertSame($expected, $this->subject->getFrom());
+       }
+
+       /**
+        * @test
+        * @param string|array $addresses
+        * @param string|array $expected
+        * @dataProvider emailAddressesDataProvider
+        */
+       public function setReplyToIdnaEncodesAddresses($addresses, $expected) {
+               $this->subject->setReplyTo($addresses);
+
+               $this->assertSame($expected, $this->subject->getReplyTo());
+       }
+
+       /**
+        * @test
+        * @param string|array $addresses
+        * @param string|array $expected
+        * @dataProvider emailAddressesDataProvider
+        */
+       public function setToIdnaEncodesAddresses($addresses, $expected) {
+               $this->subject->setTo($addresses);
+
+               $this->assertSame($expected, $this->subject->getTo());
+       }
+
+       /**
+        * @test
+        * @param string|array $addresses
+        * @param string|array $expected
+        * @dataProvider emailAddressesDataProvider
+        */
+       public function setCcIdnaEncodesAddresses($addresses, $expected) {
+               $this->subject->setCc($addresses);
+
+               $this->assertSame($expected, $this->subject->getCc());
+       }
+
+       /**
+        * @test
+        * @param string|array $addresses
+        * @param string|array $expected
+        * @dataProvider emailAddressesDataProvider
+        */
+       public function setBccIdnaEncodesAddresses($addresses, $expected) {
+               $this->subject->setBcc($addresses);
+
+               $this->assertSame($expected, $this->subject->getBcc());
+       }
+
+       /**
+        * @test
+        * @param string|array $addresses
+        * @param string|array $expected
+        * @dataProvider emailAddressesDataProvider
+        */
+       public function setReadReceiptToIdnaEncodesAddresses($addresses, $expected) {
+               $this->subject->setReadReceiptTo($addresses);
+
+               $this->assertSame($expected, $this->subject->getReadReceiptTo());
+       }
+
+}