1<?php
2
3/*
4 * This file is part of SwiftMailer.
5 * (c) 2004-2009 Chris Corbyn
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * Sends Messages using the mail() function.
13 *
14 * It is advised that users do not use this transport if at all possible
15 * since a number of plugin features cannot be used in conjunction with this
16 * transport due to the internal interface in PHP itself.
17 *
18 * The level of error reporting with this transport is incredibly weak, again
19 * due to limitations of PHP's internal mail() function.  You'll get an
20 * all-or-nothing result from sending.
21 *
22 * @author Chris Corbyn
23 *
24 * @deprecated since 5.4.5 (to be removed in 6.0)
25 */
26class Swift_Transport_MailTransport implements Swift_Transport
27{
28    /** Additional parameters to pass to mail() */
29    private $_extraParams = '-f%s';
30
31    /** The event dispatcher from the plugin API */
32    private $_eventDispatcher;
33
34    /** An invoker that calls the mail() function */
35    private $_invoker;
36
37    /**
38     * Create a new MailTransport with the $log.
39     *
40     * @param Swift_Transport_MailInvoker  $invoker
41     * @param Swift_Events_EventDispatcher $eventDispatcher
42     */
43    public function __construct(Swift_Transport_MailInvoker $invoker, Swift_Events_EventDispatcher $eventDispatcher)
44    {
45        @trigger_error(sprintf('The %s class is deprecated since version 5.4.5 and will be removed in 6.0. Use the Sendmail or SMTP transport instead.', __CLASS__), E_USER_DEPRECATED);
46
47        $this->_invoker = $invoker;
48        $this->_eventDispatcher = $eventDispatcher;
49    }
50
51    /**
52     * Not used.
53     */
54    public function isStarted()
55    {
56        return false;
57    }
58
59    /**
60     * Not used.
61     */
62    public function start()
63    {
64    }
65
66    /**
67     * Not used.
68     */
69    public function stop()
70    {
71    }
72
73    /**
74     * Set the additional parameters used on the mail() function.
75     *
76     * This string is formatted for sprintf() where %s is the sender address.
77     *
78     * @param string $params
79     *
80     * @return $this
81     */
82    public function setExtraParams($params)
83    {
84        $this->_extraParams = $params;
85
86        return $this;
87    }
88
89    /**
90     * Get the additional parameters used on the mail() function.
91     *
92     * This string is formatted for sprintf() where %s is the sender address.
93     *
94     * @return string
95     */
96    public function getExtraParams()
97    {
98        return $this->_extraParams;
99    }
100
101    /**
102     * Send the given Message.
103     *
104     * Recipient/sender data will be retrieved from the Message API.
105     * The return value is the number of recipients who were accepted for delivery.
106     *
107     * @param Swift_Mime_Message $message
108     * @param string[]           $failedRecipients An array of failures by-reference
109     *
110     * @return int
111     */
112    public function send(Swift_Mime_Message $message, &$failedRecipients = null)
113    {
114        $failedRecipients = (array) $failedRecipients;
115
116        if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) {
117            $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed');
118            if ($evt->bubbleCancelled()) {
119                return 0;
120            }
121        }
122
123        $count = (
124            count((array) $message->getTo())
125            + count((array) $message->getCc())
126            + count((array) $message->getBcc())
127            );
128
129        $toHeader = $message->getHeaders()->get('To');
130        $subjectHeader = $message->getHeaders()->get('Subject');
131
132        if (0 === $count) {
133            $this->_throwException(new Swift_TransportException('Cannot send message without a recipient'));
134        }
135        $to = $toHeader ? $toHeader->getFieldBody() : '';
136        $subject = $subjectHeader ? $subjectHeader->getFieldBody() : '';
137
138        $reversePath = $this->_getReversePath($message);
139
140        // Remove headers that would otherwise be duplicated
141        $message->getHeaders()->remove('To');
142        $message->getHeaders()->remove('Subject');
143
144        $messageStr = $message->toString();
145
146        if ($toHeader) {
147            $message->getHeaders()->set($toHeader);
148        }
149        $message->getHeaders()->set($subjectHeader);
150
151        // Separate headers from body
152        if (false !== $endHeaders = strpos($messageStr, "\r\n\r\n")) {
153            $headers = substr($messageStr, 0, $endHeaders)."\r\n"; //Keep last EOL
154            $body = substr($messageStr, $endHeaders + 4);
155        } else {
156            $headers = $messageStr."\r\n";
157            $body = '';
158        }
159
160        unset($messageStr);
161
162        if ("\r\n" != PHP_EOL) {
163            // Non-windows (not using SMTP)
164            $headers = str_replace("\r\n", PHP_EOL, $headers);
165            $subject = str_replace("\r\n", PHP_EOL, $subject);
166            $body = str_replace("\r\n", PHP_EOL, $body);
167            $to = str_replace("\r\n", PHP_EOL, $to);
168        } else {
169            // Windows, using SMTP
170            $headers = str_replace("\r\n.", "\r\n..", $headers);
171            $subject = str_replace("\r\n.", "\r\n..", $subject);
172            $body = str_replace("\r\n.", "\r\n..", $body);
173            $to = str_replace("\r\n.", "\r\n..", $to);
174        }
175
176        if ($this->_invoker->mail($to, $subject, $body, $headers, $this->_formatExtraParams($this->_extraParams, $reversePath))) {
177            if ($evt) {
178                $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS);
179                $evt->setFailedRecipients($failedRecipients);
180                $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed');
181            }
182        } else {
183            $failedRecipients = array_merge(
184                $failedRecipients,
185                array_keys((array) $message->getTo()),
186                array_keys((array) $message->getCc()),
187                array_keys((array) $message->getBcc())
188                );
189
190            if ($evt) {
191                $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED);
192                $evt->setFailedRecipients($failedRecipients);
193                $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed');
194            }
195
196            $message->generateId();
197
198            $count = 0;
199        }
200
201        return $count;
202    }
203
204    /**
205     * Register a plugin.
206     *
207     * @param Swift_Events_EventListener $plugin
208     */
209    public function registerPlugin(Swift_Events_EventListener $plugin)
210    {
211        $this->_eventDispatcher->bindEventListener($plugin);
212    }
213
214    /** Throw a TransportException, first sending it to any listeners */
215    protected function _throwException(Swift_TransportException $e)
216    {
217        if ($evt = $this->_eventDispatcher->createTransportExceptionEvent($this, $e)) {
218            $this->_eventDispatcher->dispatchEvent($evt, 'exceptionThrown');
219            if (!$evt->bubbleCancelled()) {
220                throw $e;
221            }
222        } else {
223            throw $e;
224        }
225    }
226
227    /** Determine the best-use reverse path for this message */
228    private function _getReversePath(Swift_Mime_Message $message)
229    {
230        $return = $message->getReturnPath();
231        $sender = $message->getSender();
232        $from = $message->getFrom();
233        $path = null;
234        if (!empty($return)) {
235            $path = $return;
236        } elseif (!empty($sender)) {
237            $keys = array_keys($sender);
238            $path = array_shift($keys);
239        } elseif (!empty($from)) {
240            $keys = array_keys($from);
241            $path = array_shift($keys);
242        }
243
244        return $path;
245    }
246
247    /**
248     * Fix CVE-2016-10074 by disallowing potentially unsafe shell characters.
249     *
250     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
251     *
252     * @param string $string The string to be validated
253     *
254     * @return bool
255     */
256    private function _isShellSafe($string)
257    {
258        // Future-proof
259        if (escapeshellcmd($string) !== $string || !in_array(escapeshellarg($string), array("'$string'", "\"$string\""))) {
260            return false;
261        }
262
263        $length = strlen($string);
264        for ($i = 0; $i < $length; ++$i) {
265            $c = $string[$i];
266            // All other characters have a special meaning in at least one common shell, including = and +.
267            // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
268            // Note that this does permit non-Latin alphanumeric characters based on the current locale.
269            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
270                return false;
271            }
272        }
273
274        return true;
275    }
276
277    /**
278     * Return php mail extra params to use for invoker->mail.
279     *
280     * @param $extraParams
281     * @param $reversePath
282     *
283     * @return string|null
284     */
285    private function _formatExtraParams($extraParams, $reversePath)
286    {
287        if (false !== strpos($extraParams, '-f%s')) {
288            if (empty($reversePath) || false === $this->_isShellSafe($reversePath)) {
289                $extraParams = str_replace('-f%s', '', $extraParams);
290            } else {
291                $extraParams = sprintf($extraParams, $reversePath);
292            }
293        }
294
295        return !empty($extraParams) ? $extraParams : null;
296    }
297}
298