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