[BUGFIX] swiftmaileradapter should ignore empty headers
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Mail / SwiftMailerAdapter.php
1 <?php
2 namespace TYPO3\CMS\Core\Mail;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2010-2013 Jigal van Hemert <jigal@xs4all.nl>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the textfile GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29
30 /**
31 * Hook subscriber for using Swift Mailer with the t3lib_utility_mail function
32 *
33 * @author Jigal van Hemert <jigal@xs4all.nl>
34 */
35 class SwiftMailerAdapter implements \TYPO3\CMS\Core\Mail\MailerAdapterInterface {
36
37 /**
38 * @var $mailer \TYPO3\CMS\Core\Mail\Mailer
39 */
40 protected $mailer;
41
42 /**
43 * @var $message Swift_Message
44 */
45 protected $message;
46
47 /**
48 * @var $messageHeaders Swift_Mime_HeaderSet
49 */
50 protected $messageHeaders;
51
52 /**
53 * @var string
54 */
55 protected $boundary = '';
56
57 /**
58 * Constructor
59 *
60 * @return void
61 */
62 public function __construct() {
63 // create mailer object
64 $this->mailer = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Mail\\Mailer');
65 // create message object
66 $this->message = \Swift_Message::newInstance();
67 }
68
69 /**
70 * Parses parts of the mail message and sends it with the Swift Mailer functions
71 *
72 * @param string $to Email address to send the message to
73 * @param string $subject Subject of mail message
74 * @param string $messageBody Raw body (may be multipart)
75 * @param array $additionalHeaders Additional mail headers
76 * @param array $additionalParameters Extra parameters for the mail() command
77 * @param boolean $fakeSending If set fake sending a mail
78 * @throws \TYPO3\CMS\Core\Exception
79 * @return bool
80 */
81 public function mail($to, $subject, $messageBody, $additionalHeaders = NULL, $additionalParameters = NULL, $fakeSending = FALSE) {
82 // report success for fake sending
83 if ($fakeSending === TRUE) {
84 return TRUE;
85 }
86 $this->message->setSubject($subject);
87 // handle recipients
88 $toAddresses = $this->parseAddresses($to);
89 $this->message->setTo($toAddresses);
90 // handle additional headers
91 $headers = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(LF, $additionalHeaders, TRUE);
92 $this->messageHeaders = $this->message->getHeaders();
93 foreach ($headers as $header) {
94 list($headerName, $headerValue) = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(':', $header, FALSE, 2);
95 $this->setHeader($headerName, $headerValue);
96 }
97 // handle additional parameters (force return path)
98 if (preg_match('/-f\\s*(\\S*?)/', $additionalParameters, $matches)) {
99 $this->message->setReturnPath($this->unescapeShellArguments($matches[1]));
100 }
101 // handle from:
102 $this->fixSender();
103 // handle message body
104 $this->setBody($messageBody);
105 // send mail
106 $result = $this->mailer->send($this->message);
107 // report success/failure
108 return (bool) $result;
109 }
110
111 /**
112 * Tries to undo the action by escapeshellarg()
113 *
114 * @param string $escapedString String escaped by escapeshellarg()
115 * @return string String with escapeshellarg() action undone as best as possible
116 */
117 protected function unescapeShellArguments($escapedString) {
118 if (TYPO3_OS === 'WIN') {
119 // on Windows double quotes are used and % signs are replaced by spaces
120 if (preg_match('/^"([^"]*)"$/', trim($escapedString), $matches)) {
121 $result = str_replace('\\"', '"', $matches[1]);
122 }
123 } else {
124 // on Unix-like systems single quotes are escaped
125 if (preg_match('/^\'([^' . preg_quote('\'') . ']*)\'$/', trim($escapedString), $matches)) {
126 $result = str_replace('\\\'', '\'', $matches[1]);
127 }
128 }
129 return $result;
130 }
131
132 /**
133 * Handles setting and replacing of mail headers
134 *
135 * @param string $headerName Name of header
136 * @param string $headerValue Value of header
137 * @return void
138 */
139 protected function setHeader($headerName, $headerValue) {
140 // check for boundary in headers
141 if (preg_match('/^boundary="(.*)"$/', $headerName, $matches) > 0) {
142 $this->boundary = $matches[1];
143 return;
144 }
145
146 // Ignore empty header-values (like from an 'Reply-To:' without an email-address)
147 $headerValue = trim($headerValue);
148 if (empty($headerValue)) {
149 return;
150 }
151
152 // process other, real headers
153 if ($this->messageHeaders->has($headerName)) {
154 $header = $this->messageHeaders->get($headerName);
155 $headerType = $header->getFieldType();
156 switch ($headerType) {
157 case \Swift_Mime_Header::TYPE_TEXT:
158 $header->setValue($headerValue);
159 break;
160 case \Swift_Mime_Header::TYPE_PARAMETERIZED:
161 $header->setValue(rtrim($headerValue, ';'));
162 break;
163 case \Swift_Mime_Header::TYPE_MAILBOX:
164 $addressList = $this->parseAddresses($headerValue);
165 if (count($addressList) > 0) {
166 $header->setNameAddresses($addressList);
167 }
168 break;
169 case \Swift_Mime_Header::TYPE_DATE:
170 $header->setTimeStamp(strtotime($headerValue));
171 break;
172 case \Swift_Mime_Header::TYPE_ID:
173 // remove '<' and '>' from ID headers
174 $header->setId(trim($headerValue, '<>'));
175 break;
176 case \Swift_Mime_Header::TYPE_PATH:
177 $header->setAddress($headerValue);
178 break;
179 }
180 } else {
181 switch ($headerName) {
182 case 'From':
183 case 'To':
184 case 'Cc':
185 case 'Bcc':
186 case 'Reply-To':
187 case 'Sender':
188 $addressList = $this->parseAddresses($headerValue);
189 if (count($addressList) > 0) {
190 $this->messageHeaders->addMailboxHeader($headerName, $addressList);
191 }
192 break;
193 case 'Date':
194 $this->messageHeaders->addDateHeader($headerName, strtotime($headerValue));
195 break;
196 case 'Message-ID':
197 // remove '<' and '>' from ID headers
198 $this->messageHeaders->addIdHeader($headerName, trim($headerValue, '<>'));
199 case 'Return-Path':
200 $this->messageHeaders->addPathHeader($headerName, $headerValue);
201 break;
202 case 'Content-Type':
203 case 'Content-Disposition':
204 $this->messageHeaders->addParameterizedHeader($headerName, rtrim($headerValue, ';'));
205 break;
206 default:
207 $this->messageHeaders->addTextheader($headerName, $headerValue);
208 break;
209 }
210 }
211 }
212
213 /**
214 * Sets body of mail message. Handles multi-part and single part messages. Encoded body parts are decoded prior to adding
215 * them to the message object.
216 *
217 * @param string $body Raw body, may be multi-part
218 * @return void
219 */
220 protected function setBody($body) {
221 if ($this->boundary) {
222 // handle multi-part
223 $bodyParts = preg_split('/--' . preg_quote($this->boundary) . '(--)?/m', $body, NULL, PREG_SPLIT_NO_EMPTY);
224 foreach ($bodyParts as $bodyPart) {
225 // skip empty parts
226 if (trim($bodyPart) == '') {
227 continue;
228 }
229 // keep leading white space when exploding the text
230 $lines = explode(LF, $bodyPart);
231 // set defaults for this part
232 $encoding = '';
233 $charset = 'utf-8';
234 $contentType = 'text/plain';
235 // skip intro messages
236 if (trim($lines[0]) == 'This is a multi-part message in MIME format.') {
237 continue;
238 }
239 // first line is empty leftover from splitting
240 array_shift($lines);
241 while (count($lines) > 0) {
242 $line = array_shift($lines);
243 if (preg_match('/^content-type:(.*);( charset=(.*))?$/i', $line, $matches)) {
244 $contentType = trim($matches[1]);
245 if ($matches[2]) {
246 $charset = trim($matches[3]);
247 }
248 } elseif (preg_match('/^content-transfer-encoding:(.*)$/i', $line, $matches)) {
249 $encoding = trim($matches[1]);
250 } elseif (strlen(trim($line)) == 0) {
251 // empty line before actual content of this part
252 break;
253 }
254 }
255 // use rest of part as body, but reverse encoding first
256 $bodyPart = $this->decode(implode(LF, $lines), $encoding);
257 $this->message->addPart($bodyPart, $contentType, $charset);
258 }
259 } else {
260 // Handle single body
261 // The headers have already been set, so use header information
262 $contentType = $this->message->getContentType();
263 $charset = $this->message->getCharset();
264 $encoding = $this->message->getEncoder()->getName();
265 // reverse encoding and set body
266 $rawBody = $this->decode($body, $encoding);
267 $this->message->setBody($rawBody, $contentType, $charset);
268 }
269 }
270
271 /**
272 * Reverts encoding of body text
273 *
274 * @param string $text Body text to be decoded
275 * @param string $encoding Encoding type to be reverted
276 * @return string Decoded message body
277 */
278 protected function decode($text, $encoding) {
279 $result = $text;
280 switch ($encoding) {
281 case 'quoted-printable':
282 $result = quoted_printable_decode($text);
283 break;
284 case 'base64':
285 $result = base64_decode($text);
286 break;
287 }
288 return $result;
289 }
290
291 /**
292 * Parses mailbox headers and turns them into an array.
293 *
294 * Mailbox headers are a comma separated list of 'name <email@example.org' combinations or plain email addresses (or a mix
295 * of these).
296 * The resulting array has key-value pairs where the key is either a number (no display name in the mailbox header) and the
297 * value is the email address, or the key is the email address and the value is the display name.
298 *
299 * @param string $rawAddresses Comma separated list of email addresses (optionally with display name)
300 * @return array Parsed list of addresses.
301 */
302 protected function parseAddresses($rawAddresses = '') {
303 /** @var $addressParser \TYPO3\CMS\Core\Mail\Rfc822AddressesParser */
304 $addressParser = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Mail\\Rfc822AddressesParser', $rawAddresses);
305 $addresses = $addressParser->parseAddressList();
306 $addressList = array();
307 foreach ($addresses as $address) {
308 if ($address->personal) {
309 // item with name found ( name <email@example.org> )
310 $addressList[$address->mailbox . '@' . $address->host] = $address->personal;
311 } else {
312 // item without name found ( email@example.org )
313 $addressList[] = $address->mailbox . '@' . $address->host;
314 }
315 }
316 return $addressList;
317 }
318
319 /**
320 * Makes sure there is a correct sender set.
321 *
322 * If there is no from header the returnpath will be used. If that also fails a fake address will be used to make sure
323 * Swift Mailer will be able to send the message. Some SMTP server will not accept mail messages without a valid sender.
324 *
325 * @return void
326 */
327 protected function fixSender() {
328 $from = $this->message->getFrom();
329 if (count($from) > 0) {
330 reset($from);
331 list($fromAddress, $fromName) = each($from);
332 } else {
333 $fromAddress = $this->message->getReturnPath();
334 $fromName = $fromAddress;
335 }
336 if (strlen($fromAddress) == 0) {
337 $fromAddress = 'no-reply@example.org';
338 $fromName = 'TYPO3 CMS';
339 }
340 $this->message->setFrom(array($fromAddress => $fromName));
341 }
342
343 }
344
345 ?>