1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Mail\Transport;
11
12use Traversable;
13use Zend\Mail;
14use Zend\Mail\Address\AddressInterface;
15use Zend\Mail\Exception;
16use Zend\Mail\Header\HeaderInterface;
17use Zend\Mail\Transport\Exception\RuntimeException;
18
19/**
20 * Class for sending email via the PHP internal mail() function
21 */
22class Sendmail implements TransportInterface
23{
24    /**
25     * Config options for sendmail parameters
26     *
27     * @var string
28     */
29    protected $parameters;
30
31    /**
32     * Callback to use when sending mail; typically, {@link mailHandler()}
33     *
34     * @var callable
35     */
36    protected $callable;
37
38    /**
39     * error information
40     * @var string
41     */
42    protected $errstr;
43
44    /**
45     * @var string
46     */
47    protected $operatingSystem;
48
49    /**
50     * Constructor.
51     *
52     * @param  null|string|array|Traversable $parameters OPTIONAL (Default: null)
53     */
54    public function __construct($parameters = null)
55    {
56        if ($parameters !== null) {
57            $this->setParameters($parameters);
58        }
59        $this->callable = array($this, 'mailHandler');
60    }
61
62    /**
63     * Set sendmail parameters
64     *
65     * Used to populate the additional_parameters argument to mail()
66     *
67     * @param  null|string|array|Traversable $parameters
68     * @throws \Zend\Mail\Exception\InvalidArgumentException
69     * @return Sendmail
70     */
71    public function setParameters($parameters)
72    {
73        if ($parameters === null || is_string($parameters)) {
74            $this->parameters = $parameters;
75            return $this;
76        }
77
78        if (!is_array($parameters) && !$parameters instanceof Traversable) {
79            throw new Exception\InvalidArgumentException(sprintf(
80                '%s expects a string, array, or Traversable object of parameters; received "%s"',
81                __METHOD__,
82                (is_object($parameters) ? get_class($parameters) : gettype($parameters))
83            ));
84        }
85
86        $string = '';
87        foreach ($parameters as $param) {
88            $string .= ' ' . $param;
89        }
90        trim($string);
91
92        $this->parameters = $string;
93        return $this;
94    }
95
96    /**
97     * Set callback to use for mail
98     *
99     * Primarily for testing purposes, but could be used to curry arguments.
100     *
101     * @param  callable $callable
102     * @throws \Zend\Mail\Exception\InvalidArgumentException
103     * @return Sendmail
104     */
105    public function setCallable($callable)
106    {
107        if (!is_callable($callable)) {
108            throw new Exception\InvalidArgumentException(sprintf(
109                '%s expects a callable argument; received "%s"',
110                __METHOD__,
111                (is_object($callable) ? get_class($callable) : gettype($callable))
112            ));
113        }
114        $this->callable = $callable;
115        return $this;
116    }
117
118    /**
119     * Send a message
120     *
121     * @param  \Zend\Mail\Message $message
122     */
123    public function send(Mail\Message $message)
124    {
125        $to      = $this->prepareRecipients($message);
126        $subject = $this->prepareSubject($message);
127        $body    = $this->prepareBody($message);
128        $headers = $this->prepareHeaders($message);
129        $params  = $this->prepareParameters($message);
130
131        // On *nix platforms, we need to replace \r\n with \n
132        // sendmail is not an SMTP server, it is a unix command - it expects LF
133        if (!$this->isWindowsOs()) {
134            $to      = str_replace("\r\n", "\n", $to);
135            $subject = str_replace("\r\n", "\n", $subject);
136            $body    = str_replace("\r\n", "\n", $body);
137            $headers = str_replace("\r\n", "\n", $headers);
138        }
139
140        call_user_func($this->callable, $to, $subject, $body, $headers, $params);
141    }
142
143    /**
144     * Prepare recipients list
145     *
146     * @param  \Zend\Mail\Message $message
147     * @throws \Zend\Mail\Exception\RuntimeException
148     * @return string
149     */
150    protected function prepareRecipients(Mail\Message $message)
151    {
152        $headers = $message->getHeaders();
153
154        if (!$headers->has('to')) {
155            throw new Exception\RuntimeException('Invalid email; contains no "To" header');
156        }
157
158        $to   = $headers->get('to');
159        $list = $to->getAddressList();
160        if (0 == count($list)) {
161            throw new Exception\RuntimeException('Invalid "To" header; contains no addresses');
162        }
163
164        // If not on Windows, return normal string
165        if (!$this->isWindowsOs()) {
166            return $to->getFieldValue(HeaderInterface::FORMAT_ENCODED);
167        }
168
169        // Otherwise, return list of emails
170        $addresses = array();
171        foreach ($list as $address) {
172            $addresses[] = $address->getEmail();
173        }
174        $addresses = implode(', ', $addresses);
175        return $addresses;
176    }
177
178    /**
179     * Prepare the subject line string
180     *
181     * @param  \Zend\Mail\Message $message
182     * @return string
183     */
184    protected function prepareSubject(Mail\Message $message)
185    {
186        $headers = $message->getHeaders();
187        if (!$headers->has('subject')) {
188            return;
189        }
190        $header = $headers->get('subject');
191        return $header->getFieldValue(HeaderInterface::FORMAT_ENCODED);
192    }
193
194    /**
195     * Prepare the body string
196     *
197     * @param  \Zend\Mail\Message $message
198     * @return string
199     */
200    protected function prepareBody(Mail\Message $message)
201    {
202        if (!$this->isWindowsOs()) {
203            // *nix platforms can simply return the body text
204            return $message->getBodyText();
205        }
206
207        // On windows, lines beginning with a full stop need to be fixed
208        $text = $message->getBodyText();
209        $text = str_replace("\n.", "\n..", $text);
210        return $text;
211    }
212
213    /**
214     * Prepare the textual representation of headers
215     *
216     * @param  \Zend\Mail\Message $message
217     * @return string
218     */
219    protected function prepareHeaders(Mail\Message $message)
220    {
221        // On Windows, simply return verbatim
222        if ($this->isWindowsOs()) {
223            return $message->getHeaders()->toString();
224        }
225
226        // On *nix platforms, strip the "to" header
227        $headers = clone $message->getHeaders();
228        $headers->removeHeader('To');
229        $headers->removeHeader('Subject');
230
231        // Sanitize the From header
232        $from = $headers->get('From');
233        if ($from) {
234            foreach ($from->getAddressList() as $address) {
235                if (preg_match('/\\\"/', $address->getEmail())) {
236                    throw new RuntimeException('Potential code injection in From header');
237                }
238            }
239        }
240        return $headers->toString();
241    }
242
243    /**
244     * Prepare additional_parameters argument
245     *
246     * Basically, overrides the MAIL FROM envelope with either the Sender or
247     * From address.
248     *
249     * @param  \Zend\Mail\Message $message
250     * @return string
251     */
252    protected function prepareParameters(Mail\Message $message)
253    {
254        if ($this->isWindowsOs()) {
255            return;
256        }
257
258        $parameters = (string) $this->parameters;
259
260        $sender = $message->getSender();
261        if ($sender instanceof AddressInterface) {
262            $parameters .= ' -f' . $sender->getEmail();
263            return $parameters;
264        }
265
266        $from = $message->getFrom();
267        if (count($from)) {
268            $from->rewind();
269            $sender      = $from->current();
270            $parameters .= ' -f' . $sender->getEmail();
271            return $parameters;
272        }
273
274        return $parameters;
275    }
276
277    /**
278     * Send mail using PHP native mail()
279     *
280     * @param  string $to
281     * @param  string $subject
282     * @param  string $message
283     * @param  string $headers
284     * @param  $parameters
285     * @throws \Zend\Mail\Exception\RuntimeException
286     */
287    public function mailHandler($to, $subject, $message, $headers, $parameters)
288    {
289        set_error_handler(array($this, 'handleMailErrors'));
290        if ($parameters === null) {
291            $result = mail($to, $subject, $message, $headers);
292        } else {
293            $result = mail($to, $subject, $message, $headers, $parameters);
294        }
295        restore_error_handler();
296
297        if ($this->errstr !== null || !$result) {
298            $errstr = $this->errstr;
299            if (empty($errstr)) {
300                $errstr = 'Unknown error';
301            }
302            throw new Exception\RuntimeException('Unable to send mail: ' . $errstr);
303        }
304    }
305
306    /**
307     * Temporary error handler for PHP native mail().
308     *
309     * @param int    $errno
310     * @param string $errstr
311     * @param string $errfile
312     * @param string $errline
313     * @param array  $errcontext
314     * @return bool always true
315     */
316    public function handleMailErrors($errno, $errstr, $errfile = null, $errline = null, array $errcontext = null)
317    {
318        $this->errstr = $errstr;
319        return true;
320    }
321
322    /**
323     * Is this a windows OS?
324     *
325     * @return bool
326     */
327    protected function isWindowsOs()
328    {
329        if (!$this->operatingSystem) {
330            $this->operatingSystem = strtoupper(substr(PHP_OS, 0, 3));
331        }
332        return ($this->operatingSystem == 'WIN');
333    }
334}
335