1<?php
2/** vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */
3// +----------------------------------------------------------------------+
4// | PHP Version 5 and 7                                                  |
5// +----------------------------------------------------------------------+
6// | Copyright (c) 1997-2021 Jon Parise and Chuck Hagenbuch               |
7// | All rights reserved.                                                 |
8// |                                                                      |
9// | Redistribution and use in source and binary forms, with or without   |
10// | modification, are permitted provided that the following conditions   |
11// | are met:                                                             |
12// |                                                                      |
13// | 1. Redistributions of source code must retain the above copyright    |
14// |    notice, this list of conditions and the following disclaimer.     |
15// |                                                                      |
16// | 2. Redistributions in binary form must reproduce the above copyright |
17// |    notice, this list of conditions and the following disclaimer in   |
18// |    the documentation and/or other materials provided with the        |
19// |    distribution.                                                     |
20// |                                                                      |
21// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS  |
22// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT    |
23// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS    |
24// | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE       |
25// | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
26// | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
27// | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;     |
28// | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER     |
29// | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT   |
30// | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN    |
31// | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE      |
32// | POSSIBILITY OF SUCH DAMAGE.                                          |
33// +----------------------------------------------------------------------+
34// | Authors: Chuck Hagenbuch <chuck@horde.org>                           |
35// |          Jon Parise <jon@php.net>                                    |
36// |          Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar>      |
37// +----------------------------------------------------------------------+
38
39require_once 'PEAR.php';
40require_once 'Net/Socket.php';
41
42/**
43 * Provides an implementation of the SMTP protocol using PEAR's
44 * Net_Socket class.
45 *
46 * @package Net_SMTP
47 * @author  Chuck Hagenbuch <chuck@horde.org>
48 * @author  Jon Parise <jon@php.net>
49 * @author  Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar>
50 * @license http://opensource.org/licenses/bsd-license.php BSD-2-Clause
51 *
52 * @example basic.php A basic implementation of the Net_SMTP package.
53 */
54class Net_SMTP
55{
56    /**
57     * The server to connect to.
58     * @var string
59     */
60    public $host = 'localhost';
61
62    /**
63     * The port to connect to.
64     * @var int
65     */
66    public $port = 25;
67
68    /**
69     * The value to give when sending EHLO or HELO.
70     * @var string
71     */
72    public $localhost = 'localhost';
73
74    /**
75     * List of supported authentication methods, in preferential order.
76     * @var array
77     */
78    public $auth_methods = array();
79
80    /**
81     * Use SMTP command pipelining (specified in RFC 2920) if the SMTP
82     * server supports it.
83     *
84     * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(),
85     * somlFrom() and samlFrom() do not wait for a response from the
86     * SMTP server but return immediately.
87     *
88     * @var bool
89     */
90    public $pipelining = false;
91
92    /**
93     * Number of pipelined commands.
94     * @var int
95     */
96    protected $pipelined_commands = 0;
97
98    /**
99     * Should debugging output be enabled?
100     * @var boolean
101     */
102    protected $debug = false;
103
104    /**
105     * Debug output handler.
106     * @var callback
107     */
108    protected $debug_handler = null;
109
110    /**
111     * The socket resource being used to connect to the SMTP server.
112     * @var resource
113     */
114    protected $socket = null;
115
116    /**
117     * Array of socket options that will be passed to Net_Socket::connect().
118     * @see stream_context_create()
119     * @var array
120     */
121    protected $socket_options = null;
122
123    /**
124     * The socket I/O timeout value in seconds.
125     * @var int
126     */
127    protected $timeout = 0;
128
129    /**
130     * The most recent server response code.
131     * @var int
132     */
133    protected $code = -1;
134
135    /**
136     * The most recent server response arguments.
137     * @var array
138     */
139    protected $arguments = array();
140
141    /**
142     * Stores the SMTP server's greeting string.
143     * @var string
144     */
145    protected $greeting = null;
146
147    /**
148     * Stores detected features of the SMTP server.
149     * @var array
150     */
151    protected $esmtp = array();
152
153    /**
154     * Instantiates a new Net_SMTP object, overriding any defaults
155     * with parameters that are passed in.
156     *
157     * If you have SSL support in PHP, you can connect to a server
158     * over SSL using an 'ssl://' prefix:
159     *
160     *   // 465 is a common smtps port.
161     *   $smtp = new Net_SMTP('ssl://mail.host.com', 465);
162     *   $smtp->connect();
163     *
164     * @param string  $host             The server to connect to.
165     * @param integer $port             The port to connect to.
166     * @param string  $localhost        The value to give when sending EHLO or HELO.
167     * @param boolean $pipelining       Use SMTP command pipelining
168     * @param integer $timeout          Socket I/O timeout in seconds.
169     * @param array   $socket_options   Socket stream_context_create() options.
170     * @param string  $gssapi_principal GSSAPI service principal name
171     * @param string  $gssapi_cname     GSSAPI credentials cache
172     *
173     * @since 1.0
174     */
175    public function __construct($host = null, $port = null, $localhost = null,
176        $pipelining = false, $timeout = 0, $socket_options = null,
177        $gssapi_principal=null, $gssapi_cname=null
178    ) {
179        if (isset($host)) {
180            $this->host = $host;
181        }
182        if (isset($port)) {
183            $this->port = $port;
184        }
185        if (isset($localhost)) {
186            $this->localhost = $localhost;
187        }
188
189        $this->pipelining       = $pipelining;
190        $this->socket           = new Net_Socket();
191        $this->socket_options   = $socket_options;
192        $this->timeout          = $timeout;
193        $this->gssapi_principal = $gssapi_principal;
194        $this->gssapi_cname     = $gssapi_cname;
195
196        /* If PHP krb5 extension is loaded, we enable GSSAPI method. */
197        if (extension_loaded('krb5')) {
198            $this->setAuthMethod('GSSAPI', array($this, 'authGSSAPI'));
199        }
200
201        /* Include the Auth_SASL package.  If the package is available, we
202         * enable the authentication methods that depend upon it. */
203        if (@include_once 'Auth/SASL.php') {
204            $this->setAuthMethod('CRAM-MD5', array($this, 'authCramMD5'));
205            $this->setAuthMethod('DIGEST-MD5', array($this, 'authDigestMD5'));
206        }
207
208        /* These standard authentication methods are always available. */
209        $this->setAuthMethod('LOGIN', array($this, 'authLogin'), false);
210        $this->setAuthMethod('PLAIN', array($this, 'authPlain'), false);
211        $this->setAuthMethod('XOAUTH2', array($this, 'authXOAuth2'), false);
212    }
213
214    /**
215     * Set the socket I/O timeout value in seconds plus microseconds.
216     *
217     * @param integer $seconds      Timeout value in seconds.
218     * @param integer $microseconds Additional value in microseconds.
219     *
220     * @since 1.5.0
221     */
222    public function setTimeout($seconds, $microseconds = 0)
223    {
224        return $this->socket->setTimeout($seconds, $microseconds);
225    }
226
227    /**
228     * Set the value of the debugging flag.
229     *
230     * @param boolean  $debug   New value for the debugging flag.
231     * @param callback $handler Debug handler callback
232     *
233     * @since 1.1.0
234     */
235    public function setDebug($debug, $handler = null)
236    {
237        $this->debug         = $debug;
238        $this->debug_handler = $handler;
239    }
240
241    /**
242     * Write the given debug text to the current debug output handler.
243     *
244     * @param string $message Debug mesage text.
245     *
246     * @since 1.3.3
247     */
248    protected function debug($message)
249    {
250        if ($this->debug) {
251            if ($this->debug_handler) {
252                call_user_func_array(
253                    $this->debug_handler, array(&$this, $message)
254                );
255            } else {
256                echo "DEBUG: $message\n";
257            }
258        }
259    }
260
261    /**
262     * Send the given string of data to the server.
263     *
264     * @param string $data The string of data to send.
265     *
266     * @return mixed The number of bytes that were actually written,
267     *               or a PEAR_Error object on failure.
268     *
269     * @since 1.1.0
270     */
271    protected function send($data)
272    {
273        $this->debug("Send: $data");
274
275        $result = $this->socket->write($data);
276        if (!$result || PEAR::isError($result)) {
277            $msg = $result ? $result->getMessage() : "unknown error";
278            return PEAR::raiseError("Failed to write to socket: $msg");
279        }
280
281        return $result;
282    }
283
284    /**
285     * Send a command to the server with an optional string of
286     * arguments.  A carriage return / linefeed (CRLF) sequence will
287     * be appended to each command string before it is sent to the
288     * SMTP server - an error will be thrown if the command string
289     * already contains any newline characters. Use send() for
290     * commands that must contain newlines.
291     *
292     * @param string $command The SMTP command to send to the server.
293     * @param string $args    A string of optional arguments to append
294     *                        to the command.
295     *
296     * @return mixed The result of the send() call.
297     *
298     * @since 1.1.0
299     */
300    protected function put($command, $args = '')
301    {
302        if (!empty($args)) {
303            $command .= ' ' . $args;
304        }
305
306        if (strcspn($command, "\r\n") !== strlen($command)) {
307            return PEAR::raiseError('Commands cannot contain newlines');
308        }
309
310        return $this->send($command . "\r\n");
311    }
312
313    /**
314     * Read a reply from the SMTP server.  The reply consists of a response
315     * code and a response message.
316     *
317     * @param mixed $valid The set of valid response codes.  These
318     *                     may be specified as an array of integer
319     *                     values or as a single integer value.
320     * @param bool  $later Do not parse the response now, but wait
321     *                     until the last command in the pipelined
322     *                     command group
323     *
324     * @return mixed True if the server returned a valid response code or
325     *               a PEAR_Error object is an error condition is reached.
326     *
327     * @since 1.1.0
328     *
329     * @see getResponse
330     */
331    protected function parseResponse($valid, $later = false)
332    {
333        $this->code      = -1;
334        $this->arguments = array();
335
336        if ($later) {
337            $this->pipelined_commands++;
338            return true;
339        }
340
341        for ($i = 0; $i <= $this->pipelined_commands; $i++) {
342            while ($line = $this->socket->readLine()) {
343                $this->debug("Recv: $line");
344
345                /* If we receive an empty line, the connection was closed. */
346                if (empty($line)) {
347                    $this->disconnect();
348                    return PEAR::raiseError('Connection was closed');
349                }
350
351                /* Read the code and store the rest in the arguments array. */
352                $code = substr($line, 0, 3);
353                $this->arguments[] = trim(substr($line, 4));
354
355                /* Check the syntax of the response code. */
356                if (is_numeric($code)) {
357                    $this->code = (int)$code;
358                } else {
359                    $this->code = -1;
360                    break;
361                }
362
363                /* If this is not a multiline response, we're done. */
364                if (substr($line, 3, 1) != '-') {
365                    break;
366                }
367            }
368        }
369
370        $this->pipelined_commands = 0;
371
372        /* Compare the server's response code with the valid code/codes. */
373        if (is_int($valid) && ($this->code === $valid)) {
374            return true;
375        } elseif (is_array($valid) && in_array($this->code, $valid, true)) {
376            return true;
377        }
378
379        return PEAR::raiseError('Invalid response code received from server', $this->code);
380    }
381
382    /**
383     * Issue an SMTP command and verify its response.
384     *
385     * @param string $command The SMTP command string or data.
386     * @param mixed  $valid   The set of valid response codes. These
387     *                        may be specified as an array of integer
388     *                        values or as a single integer value.
389     *
390     * @return mixed True on success or a PEAR_Error object on failure.
391     *
392     * @since 1.6.0
393     */
394    public function command($command, $valid)
395    {
396        if (PEAR::isError($error = $this->put($command))) {
397            return $error;
398        }
399        if (PEAR::isError($error = $this->parseResponse($valid))) {
400            return $error;
401        }
402
403        return true;
404    }
405
406    /**
407     * Return a 2-tuple containing the last response from the SMTP server.
408     *
409     * @return array A two-element array: the first element contains the
410     *               response code as an integer and the second element
411     *               contains the response's arguments as a string.
412     *
413     * @since 1.1.0
414     */
415    public function getResponse()
416    {
417        return array($this->code, join("\n", $this->arguments));
418    }
419
420    /**
421     * Return the SMTP server's greeting string.
422     *
423     * @return string A string containing the greeting string, or null if
424     *                a greeting has not been received.
425     *
426     * @since 1.3.3
427     */
428    public function getGreeting()
429    {
430        return $this->greeting;
431    }
432
433    /**
434     * Attempt to connect to the SMTP server.
435     *
436     * @param int  $timeout    The timeout value (in seconds) for the
437     *                         socket connection attempt.
438     * @param bool $persistent Should a persistent socket connection
439     *                         be used?
440     *
441     * @return mixed Returns a PEAR_Error with an error message on any
442     *               kind of failure, or true on success.
443     * @since 1.0
444     */
445    public function connect($timeout = null, $persistent = false)
446    {
447        $this->greeting = null;
448
449        $result = $this->socket->connect(
450            $this->host, $this->port, $persistent, $timeout, $this->socket_options
451        );
452
453        if (PEAR::isError($result)) {
454            return PEAR::raiseError(
455                'Failed to connect socket: ' . $result->getMessage()
456            );
457        }
458
459        /*
460         * Now that we're connected, reset the socket's timeout value for
461         * future I/O operations.  This allows us to have different socket
462         * timeout values for the initial connection (our $timeout parameter)
463         * and all other socket operations.
464         */
465        if ($this->timeout > 0) {
466            if (PEAR::isError($error = $this->setTimeout($this->timeout))) {
467                return $error;
468            }
469        }
470
471        if (PEAR::isError($error = $this->parseResponse(220))) {
472            return $error;
473        }
474
475        /* Extract and store a copy of the server's greeting string. */
476        list(, $this->greeting) = $this->getResponse();
477
478        if (PEAR::isError($error = $this->negotiate())) {
479            return $error;
480        }
481
482        return true;
483    }
484
485    /**
486     * Attempt to disconnect from the SMTP server.
487     *
488     * @return mixed Returns a PEAR_Error with an error message on any
489     *               kind of failure, or true on success.
490     * @since 1.0
491     */
492    public function disconnect()
493    {
494        if (PEAR::isError($error = $this->put('QUIT'))) {
495            return $error;
496        }
497        if (PEAR::isError($error = $this->parseResponse(221))) {
498            return $error;
499        }
500        if (PEAR::isError($error = $this->socket->disconnect())) {
501            return PEAR::raiseError(
502                'Failed to disconnect socket: ' . $error->getMessage()
503            );
504        }
505
506        return true;
507    }
508
509    /**
510     * Attempt to send the EHLO command and obtain a list of ESMTP
511     * extensions available, and failing that just send HELO.
512     *
513     * @return mixed Returns a PEAR_Error with an error message on any
514     *               kind of failure, or true on success.
515     *
516     * @since 1.1.0
517     */
518    protected function negotiate()
519    {
520        if (PEAR::isError($error = $this->put('EHLO', $this->localhost))) {
521            return $error;
522        }
523
524        if (PEAR::isError($this->parseResponse(250))) {
525            /* If the EHLO failed, try the simpler HELO command. */
526            if (PEAR::isError($error = $this->put('HELO', $this->localhost))) {
527                return $error;
528            }
529            if (PEAR::isError($this->parseResponse(250))) {
530                return PEAR::raiseError('HELO was not accepted', $this->code);
531            }
532
533            return true;
534        }
535
536        foreach ($this->arguments as $argument) {
537            $verb      = strtok($argument, ' ');
538            $len       = strlen($verb);
539            $arguments = substr($argument, $len + 1, strlen($argument) - $len - 1);
540            $this->esmtp[$verb] = $arguments;
541        }
542
543        if (!isset($this->esmtp['PIPELINING'])) {
544            $this->pipelining = false;
545        }
546
547        return true;
548    }
549
550    /**
551     * Returns the name of the best authentication method that the server
552     * has advertised.
553     *
554     * @return mixed Returns a string containing the name of the best
555     *               supported authentication method or a PEAR_Error object
556     *               if a failure condition is encountered.
557     * @since 1.1.0
558     */
559    protected function getBestAuthMethod()
560    {
561        $available_methods = explode(' ', $this->esmtp['AUTH']);
562
563        foreach ($this->auth_methods as $method => $callback) {
564            if (in_array($method, $available_methods)) {
565                return $method;
566            }
567        }
568
569        return PEAR::raiseError('No supported authentication methods');
570    }
571
572    /**
573     * Establish STARTTLS Connection.
574     *
575     * @return mixed Returns a PEAR_Error with an error message on any
576     *               kind of failure, true on success, or false if SSL/TLS
577     *               isn't available.
578     * @since 1.10.0
579     */
580    public function starttls()
581    {
582        /* We can only attempt a TLS connection if one has been requested,
583         * we're running PHP 5.1.0 or later, have access to the OpenSSL
584         * extension, are connected to an SMTP server which supports the
585         * STARTTLS extension, and aren't already connected over a secure
586         * (SSL) socket connection. */
587        if (version_compare(PHP_VERSION, '5.1.0', '>=')
588            && extension_loaded('openssl') && isset($this->esmtp['STARTTLS'])
589            && strncasecmp($this->host, 'ssl://', 6) !== 0
590            ) {
591                /* Start the TLS connection attempt. */
592                if (PEAR::isError($result = $this->put('STARTTLS'))) {
593                    return $result;
594                }
595                if (PEAR::isError($result = $this->parseResponse(220))) {
596                    return $result;
597                }
598                if (isset($this->socket_options['ssl']['crypto_method'])) {
599                    $crypto_method = $this->socket_options['ssl']['crypto_method'];
600                } else {
601                    /* STREAM_CRYPTO_METHOD_TLS_ANY_CLIENT constant does not exist
602                     * and STREAM_CRYPTO_METHOD_SSLv23_CLIENT constant is
603                     * inconsistent across PHP versions. */
604                    $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT
605                    | @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
606                    | @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
607                }
608                if (PEAR::isError($result = $this->socket->enableCrypto(true, $crypto_method))) {
609                    return $result;
610                } elseif ($result !== true) {
611                    return PEAR::raiseError('STARTTLS failed');
612                }
613
614                /* Send EHLO again to recieve the AUTH string from the
615                 * SMTP server. */
616                $this->negotiate();
617            } else {
618                return false;
619            }
620
621            return true;
622    }
623
624    /**
625     * Attempt to do SMTP authentication.
626     *
627     * @param string $uid    The userid to authenticate as.
628     * @param string $pwd    The password to authenticate with.
629     * @param string $method The requested authentication method.  If none is
630     *                       specified, the best supported method will be used.
631     * @param bool   $tls    Flag indicating whether or not TLS should be attempted.
632     * @param string $authz  An optional authorization identifier.  If specified, this
633     *                       identifier will be used as the authorization proxy.
634     *
635     * @return mixed Returns a PEAR_Error with an error message on any
636     *               kind of failure, or true on success.
637     * @since 1.0
638     */
639    public function auth($uid, $pwd , $method = '', $tls = true, $authz = '')
640    {
641        /* We can only attempt a TLS connection if one has been requested,
642         * we're running PHP 5.1.0 or later, have access to the OpenSSL
643         * extension, are connected to an SMTP server which supports the
644         * STARTTLS extension, and aren't already connected over a secure
645         * (SSL) socket connection. */
646        if ($tls) {
647            /* Start the TLS connection attempt. */
648            if (PEAR::isError($starttls = $this->starttls())) {
649                return $starttls;
650            }
651        }
652
653        if (empty($this->esmtp['AUTH'])) {
654            return PEAR::raiseError('SMTP server does not support authentication');
655        }
656
657        /* If no method has been specified, get the name of the best
658         * supported method advertised by the SMTP server. */
659        if (empty($method)) {
660            if (PEAR::isError($method = $this->getBestAuthMethod())) {
661                /* Return the PEAR_Error object from _getBestAuthMethod(). */
662                return $method;
663            }
664        } else {
665            $method = strtoupper($method);
666            if (!array_key_exists($method, $this->auth_methods)) {
667                return PEAR::raiseError("$method is not a supported authentication method");
668            }
669        }
670
671        if (!isset($this->auth_methods[$method])) {
672            return PEAR::raiseError("$method is not a supported authentication method");
673        }
674
675        if (!is_callable($this->auth_methods[$method], false)) {
676            return PEAR::raiseError("$method authentication method cannot be called");
677        }
678
679        if (is_array($this->auth_methods[$method])) {
680            list($object, $method) = $this->auth_methods[$method];
681            $result = $object->{$method}($uid, $pwd, $authz, $this);
682        } else {
683            $func   = $this->auth_methods[$method];
684            $result = $func($uid, $pwd, $authz, $this);
685        }
686
687        /* If an error was encountered, return the PEAR_Error object. */
688        if (PEAR::isError($result)) {
689            return $result;
690        }
691
692        return true;
693    }
694
695    /**
696     * Add a new authentication method.
697     *
698     * @param string $name     The authentication method name (e.g. 'PLAIN')
699     * @param mixed  $callback The authentication callback (given as the name of a
700     *                         function or as an (object, method name) array).
701     * @param bool   $prepend  Should the new method be prepended to the list of
702     *                         available methods?  This is the default behavior,
703     *                         giving the new method the highest priority.
704     *
705     * @return mixed True on success or a PEAR_Error object on failure.
706     *
707     * @since 1.6.0
708     */
709    public function setAuthMethod($name, $callback, $prepend = true)
710    {
711        if (!is_string($name)) {
712            return PEAR::raiseError('Method name is not a string');
713        }
714
715        if (!is_string($callback) && !is_array($callback)) {
716            return PEAR::raiseError('Method callback must be string or array');
717        }
718
719        if (is_array($callback)) {
720            if (!is_object($callback[0]) || !is_string($callback[1])) {
721                return PEAR::raiseError('Bad mMethod callback array');
722            }
723        }
724
725        if ($prepend) {
726            $this->auth_methods = array_merge(
727                array($name => $callback), $this->auth_methods
728            );
729        } else {
730            $this->auth_methods[$name] = $callback;
731        }
732
733        return true;
734    }
735
736    /**
737     * Authenticates the user using the DIGEST-MD5 method.
738     *
739     * @param string $uid   The userid to authenticate as.
740     * @param string $pwd   The password to authenticate with.
741     * @param string $authz The optional authorization proxy identifier.
742     *
743     * @return mixed Returns a PEAR_Error with an error message on any
744     *               kind of failure, or true on success.
745     * @since 1.1.0
746     */
747    protected function authDigestMD5($uid, $pwd, $authz = '')
748    {
749        if (PEAR::isError($error = $this->put('AUTH', 'DIGEST-MD5'))) {
750            return $error;
751        }
752        /* 334: Continue authentication request */
753        if (PEAR::isError($error = $this->parseResponse(334))) {
754            /* 503: Error: already authenticated */
755            if ($this->code === 503) {
756                return true;
757            }
758            return $error;
759        }
760
761        $auth_sasl = new Auth_SASL;
762        $digest    = $auth_sasl->factory('digest-md5');
763        $challenge = base64_decode($this->arguments[0]);
764        $auth_str  = base64_encode(
765            $digest->getResponse($uid, $pwd, $challenge, $this->host, "smtp", $authz)
766        );
767
768        if (PEAR::isError($error = $this->put($auth_str))) {
769            return $error;
770        }
771        /* 334: Continue authentication request */
772        if (PEAR::isError($error = $this->parseResponse(334))) {
773            return $error;
774        }
775
776        /* We don't use the protocol's third step because SMTP doesn't
777         * allow subsequent authentication, so we just silently ignore
778         * it. */
779        if (PEAR::isError($error = $this->put(''))) {
780            return $error;
781        }
782        /* 235: Authentication successful */
783        if (PEAR::isError($error = $this->parseResponse(235))) {
784            return $error;
785        }
786    }
787
788    /**
789     * Authenticates the user using the CRAM-MD5 method.
790     *
791     * @param string $uid   The userid to authenticate as.
792     * @param string $pwd   The password to authenticate with.
793     * @param string $authz The optional authorization proxy identifier.
794     *
795     * @return mixed Returns a PEAR_Error with an error message on any
796     *               kind of failure, or true on success.
797     * @since 1.1.0
798     */
799    protected function authCRAMMD5($uid, $pwd, $authz = '')
800    {
801        if (PEAR::isError($error = $this->put('AUTH', 'CRAM-MD5'))) {
802            return $error;
803        }
804        /* 334: Continue authentication request */
805        if (PEAR::isError($error = $this->parseResponse(334))) {
806            /* 503: Error: already authenticated */
807            if ($this->code === 503) {
808                return true;
809            }
810            return $error;
811        }
812
813        $auth_sasl = new Auth_SASL;
814        $challenge = base64_decode($this->arguments[0]);
815        $cram      = $auth_sasl->factory('cram-md5');
816        $auth_str  = base64_encode($cram->getResponse($uid, $pwd, $challenge));
817
818        if (PEAR::isError($error = $this->put($auth_str))) {
819            return $error;
820        }
821
822        /* 235: Authentication successful */
823        if (PEAR::isError($error = $this->parseResponse(235))) {
824            return $error;
825        }
826    }
827
828    /**
829     * Authenticates the user using the LOGIN method.
830     *
831     * @param string $uid   The userid to authenticate as.
832     * @param string $pwd   The password to authenticate with.
833     * @param string $authz The optional authorization proxy identifier.
834     *
835     * @return mixed Returns a PEAR_Error with an error message on any
836     *               kind of failure, or true on success.
837     * @since 1.1.0
838     */
839    protected function authLogin($uid, $pwd, $authz = '')
840    {
841        if (PEAR::isError($error = $this->put('AUTH', 'LOGIN'))) {
842            return $error;
843        }
844        /* 334: Continue authentication request */
845        if (PEAR::isError($error = $this->parseResponse(334))) {
846            /* 503: Error: already authenticated */
847            if ($this->code === 503) {
848                return true;
849            }
850            return $error;
851        }
852
853        if (PEAR::isError($error = $this->put(base64_encode($uid)))) {
854            return $error;
855        }
856        /* 334: Continue authentication request */
857        if (PEAR::isError($error = $this->parseResponse(334))) {
858            return $error;
859        }
860
861        if (PEAR::isError($error = $this->put(base64_encode($pwd)))) {
862            return $error;
863        }
864
865        /* 235: Authentication successful */
866        if (PEAR::isError($error = $this->parseResponse(235))) {
867            return $error;
868        }
869
870        return true;
871    }
872
873    /**
874     * Authenticates the user using the PLAIN method.
875     *
876     * @param string $uid   The userid to authenticate as.
877     * @param string $pwd   The password to authenticate with.
878     * @param string $authz The optional authorization proxy identifier.
879     *
880     * @return mixed Returns a PEAR_Error with an error message on any
881     *               kind of failure, or true on success.
882     * @since 1.1.0
883     */
884    protected function authPlain($uid, $pwd, $authz = '')
885    {
886        if (PEAR::isError($error = $this->put('AUTH', 'PLAIN'))) {
887            return $error;
888        }
889        /* 334: Continue authentication request */
890        if (PEAR::isError($error = $this->parseResponse(334))) {
891            /* 503: Error: already authenticated */
892            if ($this->code === 503) {
893                return true;
894            }
895            return $error;
896        }
897
898        $auth_str = base64_encode($authz . chr(0) . $uid . chr(0) . $pwd);
899
900        if (PEAR::isError($error = $this->put($auth_str))) {
901            return $error;
902        }
903
904        /* 235: Authentication successful */
905        if (PEAR::isError($error = $this->parseResponse(235))) {
906            return $error;
907        }
908
909        return true;
910    }
911
912     /**
913     * Authenticates the user using the GSSAPI method.
914     *
915     * PHP krb5 extension is required,
916     * service principal and credentials cache must be set.
917     *
918     * @param string $uid   The userid to authenticate as.
919     * @param string $pwd   The password to authenticate with.
920     * @param string $authz The optional authorization proxy identifier.
921     *
922     * @return mixed Returns a PEAR_Error with an error message on any
923     *               kind of failure, or true on success.
924     */
925    protected function authGSSAPI($uid, $pwd, $authz = '')
926    {
927        if (PEAR::isError($error = $this->put('AUTH', 'GSSAPI'))) {
928            return $error;
929        }
930        /* 334: Continue authentication request */
931        if (PEAR::isError($error = $this->parseResponse(334))) {
932            /* 503: Error: already authenticated */
933            if ($this->code === 503) {
934                return true;
935            }
936            return $error;
937        }
938
939        if (!$this->gssapi_principal) {
940            return PEAR::raiseError('No Kerberos service principal set', 2);
941        }
942
943        if (!empty($this->gssapi_cname)) {
944            putenv('KRB5CCNAME=' . $this->gssapi_cname);
945        }
946
947        try {
948            $ccache = new KRB5CCache();
949            if (!empty($this->gssapi_cname)) {
950                $ccache->open($this->gssapi_cname);
951            }
952
953            $gssapicontext = new GSSAPIContext();
954            $gssapicontext->acquireCredentials($ccache);
955
956            $token   = '';
957            $success = $gssapicontext->initSecContext($this->gssapi_principal, null, null, null, $token);
958            $token   = base64_encode($token);
959        }
960        catch (Exception $e) {
961            return PEAR::raiseError('GSSAPI authentication failed: ' . $e->getMessage());
962        }
963
964        if (PEAR::isError($error = $this->put($token))) {
965            return $error;
966        }
967
968        /* 334: Continue authentication request */
969        if (PEAR::isError($error = $this->parseResponse(334))) {
970            return $error;
971        }
972
973        $response = $this->arguments[0];
974
975        try {
976            $challenge = base64_decode($response);
977            $gssapicontext->unwrap($challenge, $challenge);
978            $gssapicontext->wrap($challenge, $challenge, true);
979        }
980        catch (Exception $e) {
981            return PEAR::raiseError('GSSAPI authentication failed: ' . $e->getMessage());
982        }
983
984        if (PEAR::isError($error = $this->put(base64_encode($challenge)))) {
985            return $error;
986        }
987
988        /* 235: Authentication successful */
989        if (PEAR::isError($error = $this->parseResponse(235))) {
990            return $error;
991        }
992
993        return true;
994    }
995
996    /**
997     * Authenticates the user using the XOAUTH2 method.
998     *
999     * @param string $uid   The userid to authenticate as.
1000     * @param string $token The access token to authenticate with.
1001     * @param string $authz The optional authorization proxy identifier.
1002     *
1003     * @return mixed Returns a PEAR_Error with an error message on any
1004     *               kind of failure, or true on success.
1005     * @since 1.9.0
1006     */
1007    public function authXOAuth2($uid, $token, $authz, $conn)
1008    {
1009        $auth = base64_encode("user=$uid\1auth=$token\1\1");
1010        if (PEAR::isError($error = $this->put('AUTH', 'XOAUTH2 ' . $auth))) {
1011            return $error;
1012        }
1013
1014        /* 235: Authentication successful or 334: Continue authentication */
1015        if (PEAR::isError($error = $this->parseResponse([235, 334]))) {
1016            return $error;
1017        }
1018
1019        /* 334: Continue authentication request */
1020        if ($this->code === 334) {
1021            /* Send an empty line as response to 334 */
1022            if (PEAR::isError($error = $this->put(''))) {
1023                return $error;
1024            }
1025
1026            /* Expect 235: Authentication successful */
1027            if (PEAR::isError($error = $this->parseResponse(235))) {
1028                return $error;
1029            }
1030        }
1031
1032        return true;
1033    }
1034
1035    /**
1036     * Send the HELO command.
1037     *
1038     * @param string $domain The domain name to say we are.
1039     *
1040     * @return mixed Returns a PEAR_Error with an error message on any
1041     *               kind of failure, or true on success.
1042     * @since 1.0
1043     */
1044    public function helo($domain)
1045    {
1046        if (PEAR::isError($error = $this->put('HELO', $domain))) {
1047            return $error;
1048        }
1049        if (PEAR::isError($error = $this->parseResponse(250))) {
1050            return $error;
1051        }
1052
1053        return true;
1054    }
1055
1056    /**
1057     * Return the list of SMTP service extensions advertised by the server.
1058     *
1059     * @return array The list of SMTP service extensions.
1060     * @since 1.3
1061     */
1062    public function getServiceExtensions()
1063    {
1064        return $this->esmtp;
1065    }
1066
1067    /**
1068     * Send the MAIL FROM: command.
1069     *
1070     * @param string $sender The sender (reverse path) to set.
1071     * @param string $params String containing additional MAIL parameters,
1072     *                       such as the NOTIFY flags defined by RFC 1891
1073     *                       or the VERP protocol.
1074     *
1075     *                       If $params is an array, only the 'verp' option
1076     *                       is supported.  If 'verp' is true, the XVERP
1077     *                       parameter is appended to the MAIL command.
1078     *                       If the 'verp' value is a string, the full
1079     *                       XVERP=value parameter is appended.
1080     *
1081     * @return mixed Returns a PEAR_Error with an error message on any
1082     *               kind of failure, or true on success.
1083     * @since 1.0
1084     */
1085    public function mailFrom($sender, $params = null)
1086    {
1087        $args = "FROM:<$sender>";
1088
1089        /* Support the deprecated array form of $params. */
1090        if (is_array($params) && isset($params['verp'])) {
1091            if ($params['verp'] === true) {
1092                $args .= ' XVERP';
1093            } elseif (trim($params['verp'])) {
1094                $args .= ' XVERP=' . $params['verp'];
1095            }
1096        } elseif (is_string($params) && !empty($params)) {
1097            $args .= ' ' . $params;
1098        }
1099
1100        if (PEAR::isError($error = $this->put('MAIL', $args))) {
1101            return $error;
1102        }
1103        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1104            return $error;
1105        }
1106
1107        return true;
1108    }
1109
1110    /**
1111     * Send the RCPT TO: command.
1112     *
1113     * @param string $recipient The recipient (forward path) to add.
1114     * @param string $params    String containing additional RCPT parameters,
1115     *                          such as the NOTIFY flags defined by RFC 1891.
1116     *
1117     * @return mixed Returns a PEAR_Error with an error message on any
1118     *               kind of failure, or true on success.
1119     *
1120     * @since 1.0
1121     */
1122    public function rcptTo($recipient, $params = null)
1123    {
1124        $args = "TO:<$recipient>";
1125        if (is_string($params)) {
1126            $args .= ' ' . $params;
1127        }
1128
1129        if (PEAR::isError($error = $this->put('RCPT', $args))) {
1130            return $error;
1131        }
1132        if (PEAR::isError($error = $this->parseResponse(array(250, 251), $this->pipelining))) {
1133            return $error;
1134        }
1135
1136        return true;
1137    }
1138
1139    /**
1140     * Quote the data so that it meets SMTP standards.
1141     *
1142     * This is provided as a separate public function to facilitate
1143     * easier overloading for the cases where it is desirable to
1144     * customize the quoting behavior.
1145     *
1146     * @param string &$data The message text to quote. The string must be passed
1147     *                      by reference, and the text will be modified in place.
1148     *
1149     * @since 1.2
1150     */
1151    public function quotedata(&$data)
1152    {
1153        /* Because a single leading period (.) signifies an end to the
1154         * data, legitimate leading periods need to be "doubled" ('..'). */
1155        $data = preg_replace('/^\./m', '..', $data);
1156
1157        /* Change Unix (\n) and Mac (\r) linefeeds into CRLF's (\r\n). */
1158        $data = preg_replace('/(?:\r\n|\n|\r(?!\n))/', "\r\n", $data);
1159    }
1160
1161    /**
1162     * Send the DATA command.
1163     *
1164     * @param mixed  $data    The message data, either as a string or an open
1165     *                        file resource.
1166     * @param string $headers The message headers.  If $headers is provided,
1167     *                        $data is assumed to contain only body data.
1168     *
1169     * @return mixed Returns a PEAR_Error with an error message on any
1170     *               kind of failure, or true on success.
1171     * @since 1.0
1172     */
1173    public function data($data, $headers = null)
1174    {
1175        /* Verify that $data is a supported type. */
1176        if (!is_string($data) && !is_resource($data)) {
1177            return PEAR::raiseError('Expected a string or file resource');
1178        }
1179
1180        /* Start by considering the size of the optional headers string.  We
1181         * also account for the addition 4 character "\r\n\r\n" separator
1182         * sequence. */
1183        $size = $headers_size = (is_null($headers)) ? 0 : strlen($headers) + 4;
1184
1185        if (is_resource($data)) {
1186            $stat = fstat($data);
1187            if ($stat === false) {
1188                return PEAR::raiseError('Failed to get file size');
1189            }
1190            $size += $stat['size'];
1191        } else {
1192            $size += strlen($data);
1193        }
1194
1195        /* RFC 1870, section 3, subsection 3 states "a value of zero indicates
1196         * that no fixed maximum message size is in force".  Furthermore, it
1197         * says that if "the parameter is omitted no information is conveyed
1198         * about the server's fixed maximum message size". */
1199        $limit = (isset($this->esmtp['SIZE'])) ? $this->esmtp['SIZE'] : 0;
1200        if ($limit > 0 && $size >= $limit) {
1201            return PEAR::raiseError('Message size exceeds server limit');
1202        }
1203
1204        /* Initiate the DATA command. */
1205        if (PEAR::isError($error = $this->put('DATA'))) {
1206            return $error;
1207        }
1208        if (PEAR::isError($error = $this->parseResponse(354))) {
1209            return $error;
1210        }
1211
1212        /* If we have a separate headers string, send it first. */
1213        if (!is_null($headers)) {
1214            $this->quotedata($headers);
1215            if (PEAR::isError($result = $this->send($headers . "\r\n\r\n"))) {
1216                return $result;
1217            }
1218
1219            /* Subtract the headers size now that they've been sent. */
1220            $size -= $headers_size;
1221        }
1222
1223        /* Now we can send the message body data. */
1224        if (is_resource($data)) {
1225            /* Stream the contents of the file resource out over our socket
1226             * connection, line by line.  Each line must be run through the
1227             * quoting routine. */
1228            while (strlen($line = fread($data, 8192)) > 0) {
1229                /* If the last character is an newline, we need to grab the
1230                 * next character to check to see if it is a period. */
1231                while (!feof($data)) {
1232                    $char = fread($data, 1);
1233                    $line .= $char;
1234                    if ($char != "\n") {
1235                        break;
1236                    }
1237                }
1238                $this->quotedata($line);
1239                if (PEAR::isError($result = $this->send($line))) {
1240                    return $result;
1241                }
1242            }
1243
1244             $last = $line;
1245        } else {
1246            /*
1247             * Break up the data by sending one chunk (up to 512k) at a time.
1248             * This approach reduces our peak memory usage.
1249             */
1250            for ($offset = 0; $offset < $size;) {
1251                $end = $offset + 512000;
1252
1253                /*
1254                 * Ensure we don't read beyond our data size or span multiple
1255                 * lines.  quotedata() can't properly handle character data
1256                 * that's split across two line break boundaries.
1257                 */
1258                if ($end >= $size) {
1259                    $end = $size;
1260                } else {
1261                    for (; $end < $size; $end++) {
1262                        if ($data[$end] != "\n") {
1263                            break;
1264                        }
1265                    }
1266                }
1267
1268                /* Extract our chunk and run it through the quoting routine. */
1269                $chunk = substr($data, $offset, $end - $offset);
1270                $this->quotedata($chunk);
1271
1272                /* If we run into a problem along the way, abort. */
1273                if (PEAR::isError($result = $this->send($chunk))) {
1274                    return $result;
1275                }
1276
1277                /* Advance the offset to the end of this chunk. */
1278                $offset = $end;
1279            }
1280
1281            $last = $chunk;
1282        }
1283
1284        /* Don't add another CRLF sequence if it's already in the data */
1285        $terminator = (substr($last, -2) == "\r\n" ? '' : "\r\n") . ".\r\n";
1286
1287        /* Finally, send the DATA terminator sequence. */
1288        if (PEAR::isError($result = $this->send($terminator))) {
1289            return $result;
1290        }
1291
1292        /* Verify that the data was successfully received by the server. */
1293        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1294            return $error;
1295        }
1296
1297        return true;
1298    }
1299
1300    /**
1301     * Send the SEND FROM: command.
1302     *
1303     * @param string $path The reverse path to send.
1304     *
1305     * @return mixed Returns a PEAR_Error with an error message on any
1306     *               kind of failure, or true on success.
1307     * @since 1.2.6
1308     */
1309    public function sendFrom($path)
1310    {
1311        if (PEAR::isError($error = $this->put('SEND', "FROM:<$path>"))) {
1312            return $error;
1313        }
1314        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1315            return $error;
1316        }
1317
1318        return true;
1319    }
1320
1321    /**
1322     * Send the SOML FROM: command.
1323     *
1324     * @param string $path The reverse path to send.
1325     *
1326     * @return mixed Returns a PEAR_Error with an error message on any
1327     *               kind of failure, or true on success.
1328     * @since 1.2.6
1329     */
1330    public function somlFrom($path)
1331    {
1332        if (PEAR::isError($error = $this->put('SOML', "FROM:<$path>"))) {
1333            return $error;
1334        }
1335        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1336            return $error;
1337        }
1338
1339        return true;
1340    }
1341
1342    /**
1343     * Send the SAML FROM: command.
1344     *
1345     * @param string $path The reverse path to send.
1346     *
1347     * @return mixed Returns a PEAR_Error with an error message on any
1348     *               kind of failure, or true on success.
1349     * @since 1.2.6
1350     */
1351    public function samlFrom($path)
1352    {
1353        if (PEAR::isError($error = $this->put('SAML', "FROM:<$path>"))) {
1354            return $error;
1355        }
1356        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1357            return $error;
1358        }
1359
1360        return true;
1361    }
1362
1363    /**
1364     * Send the RSET command.
1365     *
1366     * @return mixed Returns a PEAR_Error with an error message on any
1367     *               kind of failure, or true on success.
1368     * @since  1.0
1369     */
1370    public function rset()
1371    {
1372        if (PEAR::isError($error = $this->put('RSET'))) {
1373            return $error;
1374        }
1375        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1376            return $error;
1377        }
1378
1379        return true;
1380    }
1381
1382    /**
1383     * Send the VRFY command.
1384     *
1385     * @param string $string The string to verify
1386     *
1387     * @return mixed Returns a PEAR_Error with an error message on any
1388     *               kind of failure, or true on success.
1389     * @since 1.0
1390     */
1391    public function vrfy($string)
1392    {
1393        /* Note: 251 is also a valid response code */
1394        if (PEAR::isError($error = $this->put('VRFY', $string))) {
1395            return $error;
1396        }
1397        if (PEAR::isError($error = $this->parseResponse(array(250, 252)))) {
1398            return $error;
1399        }
1400
1401        return true;
1402    }
1403
1404    /**
1405     * Send the NOOP command.
1406     *
1407     * @return mixed Returns a PEAR_Error with an error message on any
1408     *               kind of failure, or true on success.
1409     * @since 1.0
1410     */
1411    public function noop()
1412    {
1413        if (PEAR::isError($error = $this->put('NOOP'))) {
1414            return $error;
1415        }
1416        if (PEAR::isError($error = $this->parseResponse(250))) {
1417            return $error;
1418        }
1419
1420        return true;
1421    }
1422
1423    /**
1424     * Backwards-compatibility method.  identifySender()'s functionality is
1425     * now handled internally.
1426     *
1427     * @return boolean This method always return true.
1428     *
1429     * @since 1.0
1430     */
1431    public function identifySender()
1432    {
1433        return true;
1434    }
1435}
1436