1<?php
2/**
3 * Copyright 2010-2017 Horde LLC (http://www.horde.org/)
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * o Redistributions of source code must retain the above copyright
11 *   notice, this list of conditions and the following disclaimer.
12 * o Redistributions in binary form must reproduce the above copyright
13 *   notice, this list of conditions and the following disclaimer in the
14 *   documentation and/or other materials provided with the distribution.
15 * o The names of the authors may not be used to endorse or promote
16 *   products derived from this software without specific prior written
17 *   permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 *
31 * @category  Horde
32 * @copyright 2010-2017 Horde LLC
33 * @license   http://www.horde.org/licenses/bsd New BSD License
34 * @package   Mail
35 */
36
37/**
38 * SMTP implementation.
39 *
40 * @author     Chuck Hagenbuch <chuck@horde.org>
41 * @author     Jon Parise <jon@php.net>
42 * @author     Michael Slusarz <slusarz@horde.org>
43 * @category   Horde
44 * @copyright  2010-2016 Horde LLC
45 * @deprecated Use Horde_Mail_Transport_Hordesmtp instead
46 * @license    http://www.horde.org/licenses/bsd New BSD License
47 * @package    Mail
48 */
49class Horde_Mail_Transport_Smtp extends Horde_Mail_Transport
50{
51    /* Error: Failed to create a Net_SMTP object */
52    const ERROR_CREATE = 10000;
53
54    /* Error: Failed to connect to SMTP server */
55    const ERROR_CONNECT = 10001;
56
57    /* Error: SMTP authentication failure */
58    const ERROR_AUTH = 10002;
59
60    /* Error: No From: address has been provided */
61    const ERROR_FROM = 10003;
62
63    /* Error: Failed to set sender */
64    const ERROR_SENDER = 10004;
65
66    /* Error: Failed to add recipient */
67    const ERROR_RECIPIENT = 10005;
68
69    /* Error: Failed to send data */
70    const ERROR_DATA = 10006;
71
72    /**
73     * The SMTP greeting.
74     *
75     * @var string
76     */
77    public $greeting = null;
78
79    /**
80     * The SMTP queued response.
81     *
82     * @var string
83     */
84    public $queuedAs = null;
85
86    /**
87     * SMTP connection object.
88     *
89     * @var Net_SMTP
90     */
91    protected $_smtp = null;
92
93    /**
94     * The list of service extension parameters to pass to the Net_SMTP
95     * mailFrom() command.
96     *
97     * @var array
98     */
99    protected $_extparams = array();
100
101    /**
102     * Constructor.
103     *
104     * @param array $params  Additional parameters:
105     *   - auth: (mixed) SMTP authentication.
106     *           This value may be set to true, false or the name of a
107     *           specific authentication method. If the value is set to true,
108     *           the Net_SMTP package will attempt to use the best
109     *           authentication method advertised by the remote SMTP server.
110     *           DEFAULT: false.
111     *   - debug: (boolean) Activate SMTP debug mode?
112     *            DEFAULT: false
113     *   - host: (string) The server to connect to.
114     *           DEFAULT: localhost
115     *   - localhost: (string) Hostname or domain that will be sent to the
116     *                remote SMTP server in the HELO / EHLO message.
117     *                DEFAULT: localhost
118     *   - password: (string) The password to use for SMTP auth.
119     *               DEFAULT: NONE
120     *   - persist: (boolean) Should the SMTP connection persist?
121     *              DEFAULT: false
122     *   - pipelining: (boolean) Use SMTP command pipelining.
123     *                 Use SMTP command pipelining (specified in RFC 2920) if
124     *                 the SMTP server supports it. This speeds up delivery
125     *                 over high-latency connections.
126     *                 DEFAULT: false (use default value from Net_SMTP)
127     *   - port: (integer) The port to connect to.
128     *           DEFAULT: 25
129     *   - timeout: (integer) The SMTP connection timeout.
130     *              DEFAULT: NONE
131     *   - username: (string) The username to use for SMTP auth.
132     *               DEFAULT: NONE
133     */
134    public function __construct(array $params = array())
135    {
136        $this->_params = array_merge(array(
137            'auth' => false,
138            'debug' => false,
139            'host' => 'localhost',
140            'localhost' => 'localhost',
141            'password' => '',
142            'persist' => false,
143            'pipelining' => false,
144            'port' => 25,
145            'timeout' => null,
146            'username' => ''
147        ), $params);
148
149        /* Destructor implementation to ensure that we disconnect from any
150         * potentially-alive persistent SMTP connections. */
151        register_shutdown_function(array($this, 'disconnect'));
152
153        /* SMTP requires CRLF line endings. */
154        $this->sep = "\r\n";
155    }
156
157    /**
158     */
159    public function send($recipients, array $headers, $body)
160    {
161        /* If we don't already have an SMTP object, create one. */
162        $this->getSMTPObject();
163
164        $headers = $this->_sanitizeHeaders($headers);
165
166        /* Make sure the message has a trailing newline. */
167        if (is_resource($body)) {
168            fseek($body, -1, SEEK_END);
169            switch (fgetc($body)) {
170            case "\r":
171                if (fgetc($body) != "\n") {
172                    fputs($body, "\n");
173                }
174                break;
175
176            default:
177                fputs($body, "\r\n");
178                break;
179            }
180            rewind($body);
181        } elseif (substr($body, -2, 0) != "\r\n") {
182            $body .= "\r\n";
183        }
184
185        try {
186            list($from, $textHeaders) = $this->prepareHeaders($headers);
187        } catch (Horde_Mail_Exception $e) {
188            $this->_smtp->rset();
189            throw $e;
190        }
191
192        try {
193            $from = $this->_getFrom($from, $headers);
194        } catch (Horde_Mail_Exception $e) {
195            $this->_smtp->rset();
196            throw new Horde_Mail_Exception('No From: address has been provided', self::ERROR_FROM);
197        }
198
199        $params = '';
200        foreach ($this->_extparams as $key => $val) {
201            $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val);
202        }
203
204        $res = $this->_smtp->mailFrom($from, ltrim($params));
205        if ($res instanceof PEAR_Error) {
206            $this->_error(sprintf("Failed to set sender: %s", $from), $res, self::ERROR_SENDER);
207        }
208
209        try {
210            $recipients = $this->parseRecipients($recipients);
211        } catch (Horde_Mail_Exception $e) {
212            $this->_smtp->rset();
213            throw $e;
214        }
215
216        foreach ($recipients as $recipient) {
217            $res = $this->_smtp->rcptTo($recipient);
218            if ($res instanceof PEAR_Error) {
219                $this->_error("Failed to add recipient: $recipient", $res, self::ERROR_RECIPIENT);
220            }
221        }
222
223        /* Send the message's headers and the body as SMTP data. Net_SMTP does
224         * the necessary EOL conversions. */
225        $res = $this->_smtp->data($body, $textHeaders);
226        list(,$args) = $this->_smtp->getResponse();
227
228        if (preg_match("/Ok: queued as (.*)/", $args, $queued)) {
229            $this->queuedAs = $queued[1];
230        }
231
232        /* We need the greeting; from it we can extract the authorative name
233         * of the mail server we've really connected to. Ideal if we're
234         * connecting to a round-robin of relay servers and need to track
235         * which exact one took the email */
236        $this->greeting = $this->_smtp->getGreeting();
237
238        if ($res instanceof PEAR_Error) {
239            $this->_error('Failed to send data', $res, self::ERROR_DATA);
240        }
241
242        /* If persistent connections are disabled, destroy our SMTP object. */
243        if (!$this->_params['persist']) {
244            $this->disconnect();
245        }
246    }
247
248    /**
249     * Connect to the SMTP server by instantiating a Net_SMTP object.
250     *
251     * @return Net_SMTP  The SMTP object.
252     * @throws Horde_Mail_Exception
253     */
254    public function getSMTPObject()
255    {
256        if ($this->_smtp) {
257            return $this->_smtp;
258        }
259
260        $this->_smtp = new Net_SMTP(
261            $this->_params['host'],
262            $this->_params['port'],
263            $this->_params['localhost']
264        );
265
266        /* Set pipelining. */
267        if ($this->_params['pipelining']) {
268            $this->_smtp->pipelining = true;
269        }
270
271        /* If we still don't have an SMTP object at this point, fail. */
272        if (!($this->_smtp instanceof Net_SMTP)) {
273            throw new Horde_Mail_Exception('Failed to create a Net_SMTP object', self::ERROR_CREATE);
274        }
275
276        /* Configure the SMTP connection. */
277        if ($this->_params['debug']) {
278            $this->_smtp->setDebug(true);
279        }
280
281        /* Attempt to connect to the configured SMTP server. */
282        $res = $this->_smtp->connect($this->_params['timeout']);
283        if ($res instanceof PEAR_Error) {
284            $this->_error('Failed to connect to ' . $this->_params['host'] . ':' . $this->_params['port'], $res, self::ERROR_CONNECT);
285        }
286
287        /* Attempt to authenticate if authentication has been enabled. */
288        if ($this->_params['auth']) {
289            $method = is_string($this->_params['auth'])
290                ? $this->_params['auth']
291                : '';
292
293            $res = $this->_smtp->auth($this->_params['username'], $this->_params['password'], $method);
294            if ($res instanceof PEAR_Error) {
295                $this->_error("$method authentication failure", $res, self::ERROR_AUTH);
296            }
297        }
298
299        return $this->_smtp;
300    }
301
302    /**
303     * Add parameter associated with a SMTP service extension.
304     *
305     * @param string $keyword  Extension keyword.
306     * @param string $value    Any value the keyword needs.
307     */
308    public function addServiceExtensionParameter($keyword, $value = null)
309    {
310        $this->_extparams[$keyword] = $value;
311    }
312
313    /**
314     * Disconnect and destroy the current SMTP connection.
315     *
316     * @return boolean True if the SMTP connection no longer exists.
317     */
318    public function disconnect()
319    {
320        /* If we have an SMTP object, disconnect and destroy it. */
321        if (is_object($this->_smtp) && $this->_smtp->disconnect()) {
322            $this->_smtp = null;
323        }
324
325        /* We are disconnected if we no longer have an SMTP object. */
326        return ($this->_smtp === null);
327    }
328
329    /**
330     * Build a standardized string describing the current SMTP error.
331     *
332     * @param string $text       Custom string describing the error context.
333     * @param PEAR_Error $error  PEAR_Error object.
334     * @param integer $e_code    Error code.
335     *
336     * @throws Horde_Mail_Exception
337     */
338    protected function _error($text, $error, $e_code)
339    {
340        /* Split the SMTP response into a code and a response string. */
341        list($code, $response) = $this->_smtp->getResponse();
342
343        /* Abort current SMTP transaction. */
344        $this->_smtp->rset();
345
346        /* Build our standardized error string. */
347        throw new Horde_Mail_Exception($text . ' [SMTP: ' . $error->getMessage() . " (code: $code, response: $response)]", $e_code);
348    }
349
350}
351