--- t3lib/mail/class.tx_t3lib_mail_hooks.php 2010-12-01 08:30:39.000000000 -0600 +++ t3lib/mail/class.t3lib_mail_swiftmaileradapter.php 2011-01-04 17:42:17.000000000 -0600 @@ -34,62 +34,69 @@ * @package TYPO3 * @subpackage t3lib */ -class tx_t3lib_mail_hooks { +class t3lib_mail_SwiftMailerAdapter implements t3lib_mail_MailerAdapter { - /** @var $mailerObject t3lib_mail_Mailer */ - protected $mailerObject; + /** @var $mailer t3lib_mail_Mailer */ + protected $mailer; - /** @var $messageObject Swift_Message */ - protected $messageObject; + /** @var $message Swift_Message */ + protected $message; /** @var $messageHeaders Swift_Mime_HeaderSet */ protected $messageHeaders; + /** @var string */ + protected $boundary = ''; + + /** + * Constructor + * + * @return void + */ + public function __construct() { + // create mailer object + $this->mailer = t3lib_div::makeInstance('t3lib_mail_Mailer'); + // create message object + $this->message = Swift_Message::newInstance(); + } + /** - * @param array $parameters Array with keys: 'to', 'subject', 'messageBody', 'additionalHeaders', 'additionalParameters' + * Parses parts of the mail message and sends it with the Swift Mailer functions + * + * @param string $to Email address to send the message to + * @param string $subject Subject of mail message + * @param string $messageBody Raw body (may be multipart) + * @param array $additionalHeaders Additional mail headers + * @param array $additionalParameters Extra parameters for the mail() command * @param bool $fakeSending If set fake sending a mail * @throws t3lib_exception * @return bool */ - public function sendMail(array $parameters = array(), $fakeSending = FALSE) { + public function mail($to, $subject, $messageBody, $additionalHeaders = NULL, $additionalParameters = NULL, $fakeSending = FALSE) { // report success for fake sending if ($fakeSending === TRUE) { return TRUE; } - // create mailer object - $this->mailerObject = t3lib_div::makeInstance('t3lib_mail_Mailer'); - - // create message object - $this->messageObject = Swift_Message::newInstance($parameters['subject'], $parameters['messageBody']); - $this->messageObject->setTo($parameters['to']); + $this->message->setSubject($subject); + $this->message->setTo($to); // handle additional headers - $headers = t3lib_div::trimExplode(LF, $parameters['additionalHeaders'], TRUE); - $this->messageHeaders = $this->messageObject->getHeaders(); + $headers = t3lib_div::trimExplode(LF, $additionalHeaders, TRUE); + $this->messageHeaders = $this->message->getHeaders(); foreach ($headers as $header) { list($headerName, $headerValue) = t3lib_div::trimExplode(':', $header, FALSE, 2); $this->setHeader($headerName, $headerValue); } // handle additional parameters (force return path) - if (preg_match('/-f\s*(\S*?)/', $parameters['additionalParameters'], $matches)) { - $this->messageObject->setReturnPath($this->unEscapeShellArg($matches[1])); + if (preg_match('/-f\s*(\S*?)/', $additionalParameters, $matches)) { + $this->message->setReturnPath($this->unescapeShellArguments($matches[1])); } // handle from: - $from = $this->messageObject->getFrom(); - if (count($from) > 0) { - reset($from); - list($fromAddress, $fromName) = each($from); - } else { - $fromAddress = $this->messageObject->getReturnPath(); - $fromName = $fromAddress; - } - if (strlen($fromAddress) == 0) { - $fromAddress = 'no-reply@example.org'; - $fromName = 'TYPO3 Installation'; - } - $this->messageObject->setFrom(array($fromAddress => $fromName)); + $this->fixSender(); + // handle message body + $this->setBody($messageBody); // send mail - $result = $this->mailerObject->send($this->messageObject); + $result = $this->mailer->send($this->message); // report success/failure return (bool) $result; @@ -101,7 +108,7 @@ * @param $escapedString String escaped by escapeshellarg() * @return string String with escapeshellarg() action undone as best as possible */ - protected function unEscapeShellArg($escapedString) { + protected function unescapeShellArguments($escapedString) { if (TYPO3_OS === 'WIN') { // on Windows double quotes are used and % signs are replaced by spaces if (preg_match('/^"([^"]*)"$/', trim($escapedString), $matches)) { @@ -125,6 +132,12 @@ * @return void */ protected function setHeader($headerName, $headerValue) { + // check for boundary in headers + if (preg_match('/^boundary="(.*)"$/', $headerName, $matches) > 0) { + $this->boundary = $matches[1]; + return; + } + // process other, real headers if ($this->messageHeaders->has($headerName)) { $header = $this->messageHeaders->get($headerName); $headerType = $header->getFieldType(); @@ -133,28 +146,10 @@ $header->setValue($headerValue); break; case Swift_Mime_Header::TYPE_PARAMETERIZED: - $header->setValue($headerValue); + $header->setValue(rtrim($headerValue, ';')); break; case Swift_Mime_Header::TYPE_MAILBOX: - // mailbox headers look like: - // name , othermail@example.org, ... - // pattern matches cases with and without name - // comma is added to match each item in a comma separated list - preg_match_all('/,\s*([^<]+)(<([^>]*?)>)?/', ', ' . $headerValue, $addresses, PREG_SET_ORDER); - $addressList = array(); - foreach ($addresses as $address) { - if (!$address[2]) { - // item with name found ( name ) - if (t3lib_div::validEmail($address[1])) { - $addressList[] = $address[1]; - } - } else { - // item without name found ( email@example.org ) - if (t3lib_div::validEmail($address[3])) { - $addressList[$address[3]] = $address[1]; - } - } - } + $addressList = $this->parseAddresses($headerValue); if (count($addressList) > 0) { $header->setNameAddresses($addressList); } @@ -180,25 +175,7 @@ case 'Bcc': case 'Reply-To': case 'Sender': - // mailbox headers look like: - // name , othermail@example.org, ... - // pattern matches cases with and without name - // comma is added to match each item in a comma separated list - preg_match_all('/,\s*(.*?)(<([^>]*?)>)?/', ', ' . $headerValue, $addresses, PREG_SET_ORDER); - $addressList = array(); - foreach ($addresses as $address) { - if ($address[2]) { - // item with name found ( name ) - if (t3lib_div::validEmail($address[1])) { - $addressList[] = $address[1]; - } - } else { - // item without name found ( email@example.org ) - if (t3lib_div::validEmail($address[3])) { - $addressList[$address[3]] = $address[1]; - } - } - } + $addressList = $this->parseAddresses($headerValue); if (count($addressList) > 0) { $header->addMailboxHeader($headerName, $addressList); } @@ -218,7 +195,7 @@ // parameterized headers case 'Content-Type': case 'Content-Disposition': - $this->messageHeaders->addParameterizedHeader($headerName, $headerValue); + $this->messageHeaders->addParameterizedHeader($headerName, rtrim($headerValue, ';')); break; // text headers default: @@ -227,4 +204,137 @@ } } } + + /** + * Sets body of mail message. Handles multi-part and single part messages. Encoded body parts are decoded prior to adding + * them to the message object. + * + * @param string $body Raw body, may be multi-part + * @return void + */ + protected function setBody($body) { + if ($this->boundary) { + // handle multi-part + $bodyParts = preg_split('/--' . preg_quote($this->boundary) . '(--)?/m', $body, NULL, PREG_SPLIT_NO_EMPTY); + foreach ($bodyParts as $bodyPart) { + // skip empty parts + if (trim($bodyPart) == '') { + continue; + } + // keep leading white space when exploding the text + $lines = explode(LF, $bodyPart); + // set defaults for this part + $encoding = ''; + $charset = 'utf-8'; + $contentType = 'text/plain'; + // skip intro messages + if (trim($lines[0]) == 'This is a multi-part message in MIME format.') { + continue; + } + // first line is empty leftover from splitting + array_shift($lines); + while (count($lines) > 0) { + $line = array_shift($lines); + if (preg_match('/^content-type:(.*);( charset=(.*))?$/i', $line, $matches)) { + $contentType = trim($matches[1]); + if ($matches[2]) { + $charset = trim($matches[3]); + } + } else if (preg_match('/^content-transfer-encoding:(.*)$/i', $line, $matches)) { + $encoding = trim($matches[1]); + } else if (strlen(trim($line)) == 0) { + // empty line before actual content of this part + break; + } + } + // use rest of part as body, but reverse encoding first + $bodyPart = $this->decode(implode(LF, $lines), $encoding); + $this->message->addPart($bodyPart, $contentType, $charset); + } + } else { + // Handle single body + // The headers have already been set, so use header information + $contentType = $this->message->getContentType(); + $charset = $this->message->getCharset(); + $encoding = $this->message->getEncoder(); + // reverse encoding and set body + $rawBody = $this->decode($body, $encoding); + $this->message->setBody($rawBody, $contentType, $charset); + } + } + + /** + * Reverts encoding of body text + * + * @param string $text Body text to be decoded + * @param string $encoding Encoding type to be reverted + * @return string Decoded message body + */ + protected function decode($text, $encoding) { + $result = $text; + switch ($encoding) { + case 'quoted-printable': + $result = quoted_printable_decode($text); + break; + case 'base64': + $result = base64_decode($text); + break; + } + return $result; + } + + /** + * Parses mailbox headers and turns them into an array. + * + * Mailbox headers are a comma separated list of 'name ]*?)>)?/', ', ' . $rawAddresses, $addresses, PREG_SET_ORDER); + $addressList = array(); + foreach ($addresses as $address) { + if ($address[2]) { + // item with name found ( name ) + if (t3lib_div::validEmail($address[1])) { + $addressList[] = $address[1]; + } + } else { + // item without name found ( email@example.org ) + if (t3lib_div::validEmail($address[3])) { + $addressList[$address[3]] = trim($address[1], "\n\r\t\0\x0B \""); + } + } + } + return $addressList; + } + + /** + * Makes sure there is a correct sender set. + * + * If there is no from header the returnpath will be used. If that also fails a fake address will be used to make sure + * Swift Mailer will be able to send the message. Some SMTP server will not accept mail messages without a valid sender. + * + * @return void + */ + protected function fixSender() { + $from = $this->message->getFrom(); + if (count($from) > 0) { + reset($from); + list($fromAddress, $fromName) = each($from); + } else { + $fromAddress = $this->message->getReturnPath(); + $fromName = $fromAddress; + } + if (strlen($fromAddress) == 0) { + $fromAddress = 'no-reply@example.org'; + $fromName = 'TYPO3 CMS'; + } + $this->message->setFrom(array($fromAddress => $fromName)); + } }