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