1<?php
2/**
3 * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class.
4 *
5 * PHP version 5
6 *
7 * LICENSE:
8 *
9 * Copyright (c) 2010-2017, Chuck Hagenbuch & Jon Parise
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions
14 * are met:
15 *
16 * 1. Redistributions of source code must retain the above copyright
17 *    notice, this list of conditions and the following disclaimer.
18 *
19 * 2. Redistributions in binary form must reproduce the above copyright
20 *    notice, this list of conditions and the following disclaimer in the
21 *    documentation and/or other materials provided with the distribution.
22 *
23 * 3. Neither the name of the copyright holder nor the names of its
24 *    contributors may be used to endorse or promote products derived from
25 *    this software without specific prior written permission.
26 *
27 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
33 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 *
39 * @category    HTTP
40 * @package     HTTP_Request
41 * @author      Jon Parise <jon@php.net>
42 * @author      Chuck Hagenbuch <chuck@horde.org>
43 * @copyright   2010-2017 Chuck Hagenbuch
44 * @license     http://opensource.org/licenses/BSD-3-Clause New BSD License
45 * @version     CVS: $Id$
46 * @link        http://pear.php.net/package/Mail/
47 */
48
49/** Error: Failed to create a Net_SMTP object */
50define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000);
51
52/** Error: Failed to connect to SMTP server */
53define('PEAR_MAIL_SMTP_ERROR_CONNECT', 10001);
54
55/** Error: SMTP authentication failure */
56define('PEAR_MAIL_SMTP_ERROR_AUTH', 10002);
57
58/** Error: No From: address has been provided */
59define('PEAR_MAIL_SMTP_ERROR_FROM', 10003);
60
61/** Error: Failed to set sender */
62define('PEAR_MAIL_SMTP_ERROR_SENDER', 10004);
63
64/** Error: Failed to add recipient */
65define('PEAR_MAIL_SMTP_ERROR_RECIPIENT', 10005);
66
67/** Error: Failed to send data */
68define('PEAR_MAIL_SMTP_ERROR_DATA', 10006);
69
70/**
71 * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class.
72 * @access public
73 * @package Mail
74 * @version $Revision$
75 */
76class Mail_smtp extends Mail {
77
78    /**
79     * SMTP connection object.
80     *
81     * @var object
82     * @access private
83     */
84    var $_smtp = null;
85
86    /**
87     * The list of service extension parameters to pass to the Net_SMTP
88     * mailFrom() command.
89     *
90     * @var array
91     */
92    var $_extparams = array();
93
94    /**
95     * The SMTP host to connect to.
96     *
97     * @var string
98     */
99    var $host = 'localhost';
100
101    /**
102     * The port the SMTP server is on.
103     *
104     * @var integer
105     */
106    var $port = 25;
107
108    /**
109     * Should SMTP authentication be used?
110     *
111     * This value may be set to true, false or the name of a specific
112     * authentication method.
113     *
114     * If the value is set to true, the Net_SMTP package will attempt to use
115     * the best authentication method advertised by the remote SMTP server.
116     *
117     * @var mixed
118     */
119    var $auth = false;
120
121    /**
122     * The username to use if the SMTP server requires authentication.
123     *
124     * @var string
125     */
126    var $username = '';
127
128    /**
129     * The password to use if the SMTP server requires authentication.
130     *
131     * @var string
132     */
133    var $password = '';
134
135    /**
136     * Hostname or domain that will be sent to the remote SMTP server in the
137     * HELO / EHLO message.
138     *
139     * @var string
140     */
141    var $localhost = 'localhost';
142
143    /**
144     * SMTP connection timeout value.  NULL indicates no timeout.
145     *
146     * @var integer
147     */
148    var $timeout = null;
149
150    /**
151     * Turn on Net_SMTP debugging?
152     *
153     * @var boolean $debug
154     */
155    var $debug = false;
156
157    /**
158     * Indicates whether or not the SMTP connection should persist over
159     * multiple calls to the send() method.
160     *
161     * @var boolean
162     */
163    var $persist = false;
164
165    /**
166     * Use SMTP command pipelining (specified in RFC 2920) if the SMTP server
167     * supports it. This speeds up delivery over high-latency connections. By
168     * default, use the default value supplied by Net_SMTP.
169     *
170     * @var boolean
171     */
172    var $pipelining;
173
174    /**
175     * The list of socket options
176     *
177     * @var array
178     */
179    var $socket_options = array();
180
181    /**
182     * Constructor.
183     *
184     * Instantiates a new Mail_smtp:: object based on the parameters
185     * passed in. It looks for the following parameters:
186     *     host        The server to connect to. Defaults to localhost.
187     *     port        The port to connect to. Defaults to 25.
188     *     auth        SMTP authentication.  Defaults to none.
189     *     username    The username to use for SMTP auth. No default.
190     *     password    The password to use for SMTP auth. No default.
191     *     localhost   The local hostname / domain. Defaults to localhost.
192     *     timeout     The SMTP connection timeout. Defaults to none.
193     *     verp        Whether to use VERP or not. Defaults to false.
194     *                 DEPRECATED as of 1.2.0 (use setMailParams()).
195     *     debug       Activate SMTP debug mode? Defaults to false.
196     *     persist     Should the SMTP connection persist?
197     *     pipelining  Use SMTP command pipelining
198     *
199     * If a parameter is present in the $params array, it replaces the
200     * default.
201     *
202     * @param array Hash containing any parameters different from the
203     *              defaults.
204     */
205    public function __construct($params)
206    {
207        if (isset($params['host'])) $this->host = $params['host'];
208        if (isset($params['port'])) $this->port = $params['port'];
209        if (isset($params['auth'])) $this->auth = $params['auth'];
210        if (isset($params['username'])) $this->username = $params['username'];
211        if (isset($params['password'])) $this->password = $params['password'];
212        if (isset($params['localhost'])) $this->localhost = $params['localhost'];
213        if (isset($params['timeout'])) $this->timeout = $params['timeout'];
214        if (isset($params['debug'])) $this->debug = (bool)$params['debug'];
215        if (isset($params['persist'])) $this->persist = (bool)$params['persist'];
216        if (isset($params['pipelining'])) $this->pipelining = (bool)$params['pipelining'];
217        if (isset($params['socket_options'])) $this->socket_options = $params['socket_options'];
218        // Deprecated options
219        if (isset($params['verp'])) {
220            $this->addServiceExtensionParameter('XVERP', is_bool($params['verp']) ? null : $params['verp']);
221        }
222    }
223
224    /**
225     * Destructor implementation to ensure that we disconnect from any
226     * potentially-alive persistent SMTP connections.
227     */
228    public function __destruct()
229    {
230        $this->disconnect();
231    }
232
233    /**
234     * Implements Mail::send() function using SMTP.
235     *
236     * @param mixed $recipients Either a comma-seperated list of recipients
237     *              (RFC822 compliant), or an array of recipients,
238     *              each RFC822 valid. This may contain recipients not
239     *              specified in the headers, for Bcc:, resending
240     *              messages, etc.
241     *
242     * @param array $headers The array of headers to send with the mail, in an
243     *              associative array, where the array key is the
244     *              header name (e.g., 'Subject'), and the array value
245     *              is the header value (e.g., 'test'). The header
246     *              produced from those values would be 'Subject:
247     *              test'.
248     *
249     * @param string $body The full text of the message body, including any
250     *               MIME parts, etc.
251     *
252     * @return mixed Returns true on success, or a PEAR_Error
253     *               containing a descriptive error message on
254     *               failure.
255     */
256    public function send($recipients, $headers, $body)
257    {
258        $result = $this->send_or_fail($recipients, $headers, $body);
259
260        /* If persistent connections are disabled, destroy our SMTP object. */
261        if ($this->persist === false) {
262            $this->disconnect();
263        }
264
265        return $result;
266    }
267
268    protected function send_or_fail($recipients, $headers, $body)
269    {
270        /* If we don't already have an SMTP object, create one. */
271        $result = $this->getSMTPObject();
272        if (PEAR::isError($result)) {
273            return $result;
274        }
275
276        if (!is_array($headers)) {
277            return PEAR::raiseError('$headers must be an array');
278        }
279
280        $this->_sanitizeHeaders($headers);
281
282        $headerElements = $this->prepareHeaders($headers);
283        if (is_a($headerElements, 'PEAR_Error')) {
284            $this->_smtp->rset();
285            return $headerElements;
286        }
287        list($from, $textHeaders) = $headerElements;
288
289        /* Since few MTAs are going to allow this header to be forged
290         * unless it's in the MAIL FROM: exchange, we'll use
291         * Return-Path instead of From: if it's set. */
292        if (!empty($headers['Return-Path'])) {
293            $from = $headers['Return-Path'];
294        }
295
296        if (!isset($from)) {
297            $this->_smtp->rset();
298            return PEAR::raiseError('No From: address has been provided',
299                                    PEAR_MAIL_SMTP_ERROR_FROM);
300        }
301
302        $params = null;
303        if (!empty($this->_extparams)) {
304            foreach ($this->_extparams as $key => $val) {
305                $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val);
306            }
307        }
308        if (PEAR::isError($res = $this->_smtp->mailFrom($from, ltrim($params)))) {
309            $error = $this->_error("Failed to set sender: $from", $res);
310            $this->_smtp->rset();
311            return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_SENDER);
312        }
313
314        $recipients = $this->parseRecipients($recipients);
315        if (is_a($recipients, 'PEAR_Error')) {
316            $this->_smtp->rset();
317            return $recipients;
318        }
319
320        foreach ($recipients as $recipient) {
321            $res = $this->_smtp->rcptTo($recipient);
322            if (is_a($res, 'PEAR_Error')) {
323                $error = $this->_error("Failed to add recipient: $recipient", $res);
324                $this->_smtp->rset();
325                return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_RECIPIENT);
326            }
327        }
328
329        /* Send the message's headers and the body as SMTP data. */
330        $res = $this->_smtp->data($body, $textHeaders);
331        list(,$args) = $this->_smtp->getResponse();
332
333        if (preg_match("/ queued as (.*)/", $args, $queued)) {
334            $this->queued_as = $queued[1];
335        }
336
337        /* we need the greeting; from it we can extract the authorative name of the mail server we've really connected to.
338         * ideal if we're connecting to a round-robin of relay servers and need to track which exact one took the email */
339        $this->greeting = $this->_smtp->getGreeting();
340
341        if (is_a($res, 'PEAR_Error')) {
342            $error = $this->_error('Failed to send data', $res);
343            $this->_smtp->rset();
344            return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_DATA);
345        }
346
347        return true;
348    }
349
350    /**
351     * Connect to the SMTP server by instantiating a Net_SMTP object.
352     *
353     * @return mixed Returns a reference to the Net_SMTP object on success, or
354     *               a PEAR_Error containing a descriptive error message on
355     *               failure.
356     *
357     * @since  1.2.0
358     */
359    public function getSMTPObject()
360    {
361        if (is_object($this->_smtp) !== false) {
362            return $this->_smtp;
363        }
364
365        include_once 'Net/SMTP.php';
366        $this->_smtp = new Net_SMTP($this->host,
367                                     $this->port,
368                                     $this->localhost,
369                                     $this->pipelining,
370                                     0,
371                                     $this->socket_options);
372
373        /* If we still don't have an SMTP object at this point, fail. */
374        if (is_object($this->_smtp) === false) {
375            return PEAR::raiseError('Failed to create a Net_SMTP object',
376                                    PEAR_MAIL_SMTP_ERROR_CREATE);
377        }
378
379        /* Configure the SMTP connection. */
380        if ($this->debug) {
381            $this->_smtp->setDebug(true);
382        }
383
384        /* Attempt to connect to the configured SMTP server. */
385        if (PEAR::isError($res = $this->_smtp->connect($this->timeout))) {
386            $error = $this->_error('Failed to connect to ' .
387                                   $this->host . ':' . $this->port,
388                                   $res);
389            return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_CONNECT);
390        }
391
392        /* Attempt to authenticate if authentication has been enabled. */
393        if ($this->auth) {
394            $method = is_string($this->auth) ? $this->auth : '';
395
396            if (PEAR::isError($res = $this->_smtp->auth($this->username,
397                                                        $this->password,
398                                                        $method))) {
399                $error = $this->_error("$method authentication failure",
400                                       $res);
401                $this->_smtp->rset();
402                return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_AUTH);
403            }
404        }
405
406        return $this->_smtp;
407    }
408
409    /**
410     * Add parameter associated with a SMTP service extension.
411     *
412     * @param string Extension keyword.
413     * @param string Any value the keyword needs.
414     *
415     * @since 1.2.0
416     */
417    public function addServiceExtensionParameter($keyword, $value = null)
418    {
419        $this->_extparams[$keyword] = $value;
420    }
421
422    /**
423     * Disconnect and destroy the current SMTP connection.
424     *
425     * @return boolean True if the SMTP connection no longer exists.
426     *
427     * @since  1.1.9
428     */
429    public function disconnect()
430    {
431        /* If we have an SMTP object, disconnect and destroy it. */
432        if (is_object($this->_smtp) && $this->_smtp->disconnect()) {
433            $this->_smtp = null;
434        }
435
436        /* We are disconnected if we no longer have an SMTP object. */
437        return ($this->_smtp === null);
438    }
439
440    /**
441     * Build a standardized string describing the current SMTP error.
442     *
443     * @param string $text  Custom string describing the error context.
444     * @param object $error Reference to the current PEAR_Error object.
445     *
446     * @return string       A string describing the current SMTP error.
447     *
448     * @since  1.1.7
449     */
450    protected function _error($text, $error)
451    {
452        /* Split the SMTP response into a code and a response string. */
453        list($code, $response) = $this->_smtp->getResponse();
454
455        /* Build our standardized error string. */
456        return $text
457            . ' [SMTP: ' . $error->getMessage()
458            . " (code: $code, response: $response)]";
459    }
460
461}
462