1<?php
2/** vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */
3// +----------------------------------------------------------------------+
4// | PHP Version 5 and 7                                                  |
5// +----------------------------------------------------------------------+
6// | Copyright (c) 1997-2019 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     * Attempt to do SMTP authentication.
574     *
575     * @param string $uid    The userid to authenticate as.
576     * @param string $pwd    The password to authenticate with.
577     * @param string $method The requested authentication method.  If none is
578     *                       specified, the best supported method will be used.
579     * @param bool   $tls    Flag indicating whether or not TLS should be attempted.
580     * @param string $authz  An optional authorization identifier.  If specified, this
581     *                       identifier will be used as the authorization proxy.
582     *
583     * @return mixed Returns a PEAR_Error with an error message on any
584     *               kind of failure, or true on success.
585     * @since 1.0
586     */
587    public function auth($uid, $pwd , $method = '', $tls = true, $authz = '')
588    {
589        /* We can only attempt a TLS connection if one has been requested,
590         * we're running PHP 5.1.0 or later, have access to the OpenSSL
591         * extension, are connected to an SMTP server which supports the
592         * STARTTLS extension, and aren't already connected over a secure
593         * (SSL) socket connection. */
594        if ($tls && version_compare(PHP_VERSION, '5.1.0', '>=')
595            && extension_loaded('openssl') && isset($this->esmtp['STARTTLS'])
596            && strncasecmp($this->host, 'ssl://', 6) !== 0
597        ) {
598            /* Start the TLS connection attempt. */
599            if (PEAR::isError($result = $this->put('STARTTLS'))) {
600                return $result;
601            }
602            if (PEAR::isError($result = $this->parseResponse(220))) {
603                return $result;
604            }
605            if (isset($this->socket_options['ssl']['crypto_method'])) {
606                $crypto_method = $this->socket_options['ssl']['crypto_method'];
607            } else {
608                /* STREAM_CRYPTO_METHOD_TLS_ANY_CLIENT constant does not exist
609                 * and STREAM_CRYPTO_METHOD_SSLv23_CLIENT constant is
610                 * inconsistent across PHP versions. */
611                $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT
612                                 | @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
613                                 | @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
614            }
615            if (PEAR::isError($result = $this->socket->enableCrypto(true, $crypto_method))) {
616                return $result;
617            } elseif ($result !== true) {
618                return PEAR::raiseError('STARTTLS failed');
619            }
620
621            /* Send EHLO again to recieve the AUTH string from the
622             * SMTP server. */
623            $this->negotiate();
624        }
625
626        if (empty($this->esmtp['AUTH'])) {
627            return PEAR::raiseError('SMTP server does not support authentication');
628        }
629
630        /* If no method has been specified, get the name of the best
631         * supported method advertised by the SMTP server. */
632        if (empty($method)) {
633            if (PEAR::isError($method = $this->getBestAuthMethod())) {
634                /* Return the PEAR_Error object from _getBestAuthMethod(). */
635                return $method;
636            }
637        } else {
638            $method = strtoupper($method);
639            if (!array_key_exists($method, $this->auth_methods)) {
640                return PEAR::raiseError("$method is not a supported authentication method");
641            }
642        }
643
644        if (!isset($this->auth_methods[$method])) {
645            return PEAR::raiseError("$method is not a supported authentication method");
646        }
647
648        if (!is_callable($this->auth_methods[$method], false)) {
649            return PEAR::raiseError("$method authentication method cannot be called");
650        }
651
652        if (is_array($this->auth_methods[$method])) {
653            list($object, $method) = $this->auth_methods[$method];
654            $result = $object->{$method}($uid, $pwd, $authz, $this);
655        } else {
656            $func   = $this->auth_methods[$method];
657            $result = $func($uid, $pwd, $authz, $this);
658        }
659
660        /* If an error was encountered, return the PEAR_Error object. */
661        if (PEAR::isError($result)) {
662            return $result;
663        }
664
665        return true;
666    }
667
668    /**
669     * Add a new authentication method.
670     *
671     * @param string $name     The authentication method name (e.g. 'PLAIN')
672     * @param mixed  $callback The authentication callback (given as the name of a
673     *                         function or as an (object, method name) array).
674     * @param bool   $prepend  Should the new method be prepended to the list of
675     *                         available methods?  This is the default behavior,
676     *                         giving the new method the highest priority.
677     *
678     * @return mixed True on success or a PEAR_Error object on failure.
679     *
680     * @since 1.6.0
681     */
682    public function setAuthMethod($name, $callback, $prepend = true)
683    {
684        if (!is_string($name)) {
685            return PEAR::raiseError('Method name is not a string');
686        }
687
688        if (!is_string($callback) && !is_array($callback)) {
689            return PEAR::raiseError('Method callback must be string or array');
690        }
691
692        if (is_array($callback)) {
693            if (!is_object($callback[0]) || !is_string($callback[1])) {
694                return PEAR::raiseError('Bad mMethod callback array');
695            }
696        }
697
698        if ($prepend) {
699            $this->auth_methods = array_merge(
700                array($name => $callback), $this->auth_methods
701            );
702        } else {
703            $this->auth_methods[$name] = $callback;
704        }
705
706        return true;
707    }
708
709    /**
710     * Authenticates the user using the DIGEST-MD5 method.
711     *
712     * @param string $uid   The userid to authenticate as.
713     * @param string $pwd   The password to authenticate with.
714     * @param string $authz The optional authorization proxy identifier.
715     *
716     * @return mixed Returns a PEAR_Error with an error message on any
717     *               kind of failure, or true on success.
718     * @since 1.1.0
719     */
720    protected function authDigestMD5($uid, $pwd, $authz = '')
721    {
722        if (PEAR::isError($error = $this->put('AUTH', 'DIGEST-MD5'))) {
723            return $error;
724        }
725        /* 334: Continue authentication request */
726        if (PEAR::isError($error = $this->parseResponse(334))) {
727            /* 503: Error: already authenticated */
728            if ($this->code === 503) {
729                return true;
730            }
731            return $error;
732        }
733
734        $auth_sasl = new Auth_SASL;
735        $digest    = $auth_sasl->factory('digest-md5');
736        $challenge = base64_decode($this->arguments[0]);
737        $auth_str  = base64_encode(
738            $digest->getResponse($uid, $pwd, $challenge, $this->host, "smtp", $authz)
739        );
740
741        if (PEAR::isError($error = $this->put($auth_str))) {
742            return $error;
743        }
744        /* 334: Continue authentication request */
745        if (PEAR::isError($error = $this->parseResponse(334))) {
746            return $error;
747        }
748
749        /* We don't use the protocol's third step because SMTP doesn't
750         * allow subsequent authentication, so we just silently ignore
751         * it. */
752        if (PEAR::isError($error = $this->put(''))) {
753            return $error;
754        }
755        /* 235: Authentication successful */
756        if (PEAR::isError($error = $this->parseResponse(235))) {
757            return $error;
758        }
759    }
760
761    /**
762     * Authenticates the user using the CRAM-MD5 method.
763     *
764     * @param string $uid   The userid to authenticate as.
765     * @param string $pwd   The password to authenticate with.
766     * @param string $authz The optional authorization proxy identifier.
767     *
768     * @return mixed Returns a PEAR_Error with an error message on any
769     *               kind of failure, or true on success.
770     * @since 1.1.0
771     */
772    protected function authCRAMMD5($uid, $pwd, $authz = '')
773    {
774        if (PEAR::isError($error = $this->put('AUTH', 'CRAM-MD5'))) {
775            return $error;
776        }
777        /* 334: Continue authentication request */
778        if (PEAR::isError($error = $this->parseResponse(334))) {
779            /* 503: Error: already authenticated */
780            if ($this->code === 503) {
781                return true;
782            }
783            return $error;
784        }
785
786        $auth_sasl = new Auth_SASL;
787        $challenge = base64_decode($this->arguments[0]);
788        $cram      = $auth_sasl->factory('cram-md5');
789        $auth_str  = base64_encode($cram->getResponse($uid, $pwd, $challenge));
790
791        if (PEAR::isError($error = $this->put($auth_str))) {
792            return $error;
793        }
794
795        /* 235: Authentication successful */
796        if (PEAR::isError($error = $this->parseResponse(235))) {
797            return $error;
798        }
799    }
800
801    /**
802     * Authenticates the user using the LOGIN method.
803     *
804     * @param string $uid   The userid to authenticate as.
805     * @param string $pwd   The password to authenticate with.
806     * @param string $authz The optional authorization proxy identifier.
807     *
808     * @return mixed Returns a PEAR_Error with an error message on any
809     *               kind of failure, or true on success.
810     * @since 1.1.0
811     */
812    protected function authLogin($uid, $pwd, $authz = '')
813    {
814        if (PEAR::isError($error = $this->put('AUTH', 'LOGIN'))) {
815            return $error;
816        }
817        /* 334: Continue authentication request */
818        if (PEAR::isError($error = $this->parseResponse(334))) {
819            /* 503: Error: already authenticated */
820            if ($this->code === 503) {
821                return true;
822            }
823            return $error;
824        }
825
826        if (PEAR::isError($error = $this->put(base64_encode($uid)))) {
827            return $error;
828        }
829        /* 334: Continue authentication request */
830        if (PEAR::isError($error = $this->parseResponse(334))) {
831            return $error;
832        }
833
834        if (PEAR::isError($error = $this->put(base64_encode($pwd)))) {
835            return $error;
836        }
837
838        /* 235: Authentication successful */
839        if (PEAR::isError($error = $this->parseResponse(235))) {
840            return $error;
841        }
842
843        return true;
844    }
845
846    /**
847     * Authenticates the user using the PLAIN method.
848     *
849     * @param string $uid   The userid to authenticate as.
850     * @param string $pwd   The password to authenticate with.
851     * @param string $authz The optional authorization proxy identifier.
852     *
853     * @return mixed Returns a PEAR_Error with an error message on any
854     *               kind of failure, or true on success.
855     * @since 1.1.0
856     */
857    protected function authPlain($uid, $pwd, $authz = '')
858    {
859        if (PEAR::isError($error = $this->put('AUTH', 'PLAIN'))) {
860            return $error;
861        }
862        /* 334: Continue authentication request */
863        if (PEAR::isError($error = $this->parseResponse(334))) {
864            /* 503: Error: already authenticated */
865            if ($this->code === 503) {
866                return true;
867            }
868            return $error;
869        }
870
871        $auth_str = base64_encode($authz . chr(0) . $uid . chr(0) . $pwd);
872
873        if (PEAR::isError($error = $this->put($auth_str))) {
874            return $error;
875        }
876
877        /* 235: Authentication successful */
878        if (PEAR::isError($error = $this->parseResponse(235))) {
879            return $error;
880        }
881
882        return true;
883    }
884
885     /**
886     * Authenticates the user using the GSSAPI method.
887     *
888     * PHP krb5 extension is required,
889     * service principal and credentials cache must be set.
890     *
891     * @param string $uid   The userid to authenticate as.
892     * @param string $pwd   The password to authenticate with.
893     * @param string $authz The optional authorization proxy identifier.
894     *
895     * @return mixed Returns a PEAR_Error with an error message on any
896     *               kind of failure, or true on success.
897     */
898    protected function authGSSAPI($uid, $pwd, $authz = '')
899    {
900        if (PEAR::isError($error = $this->put('AUTH', 'GSSAPI'))) {
901            return $error;
902        }
903        /* 334: Continue authentication request */
904        if (PEAR::isError($error = $this->parseResponse(334))) {
905            /* 503: Error: already authenticated */
906            if ($this->code === 503) {
907                return true;
908            }
909            return $error;
910        }
911
912        if (!$this->gssapi_principal) {
913            return PEAR::raiseError('No Kerberos service principal set', 2);
914        }
915
916        if (!empty($this->gssapi_cname)) {
917            putenv('KRB5CCNAME=' . $this->gssapi_cname);
918        }
919
920        try {
921            $ccache = new KRB5CCache();
922            if (!empty($this->gssapi_cname)) {
923                $ccache->open($this->gssapi_cname);
924            }
925
926            $gssapicontext = new GSSAPIContext();
927            $gssapicontext->acquireCredentials($ccache);
928
929            $token   = '';
930            $success = $gssapicontext->initSecContext($this->gssapi_principal, null, null, null, $token);
931            $token   = base64_encode($token);
932        }
933        catch (Exception $e) {
934            return PEAR::raiseError('GSSAPI authentication failed: ' . $e->getMessage());
935        }
936
937        if (PEAR::isError($error = $this->put($token))) {
938            return $error;
939        }
940
941        /* 334: Continue authentication request */
942        if (PEAR::isError($error = $this->parseResponse(334))) {
943            return $error;
944        }
945
946        $response = $this->arguments[0];
947
948        try {
949            $challenge = base64_decode($response);
950            $gssapicontext->unwrap($challenge, $challenge);
951            $gssapicontext->wrap($challenge, $challenge, true);
952        }
953        catch (Exception $e) {
954            return PEAR::raiseError('GSSAPI authentication failed: ' . $e->getMessage());
955        }
956
957        if (PEAR::isError($error = $this->put(base64_encode($challenge)))) {
958            return $error;
959        }
960
961        /* 235: Authentication successful */
962        if (PEAR::isError($error = $this->parseResponse(235))) {
963            return $error;
964        }
965
966        return true;
967    }
968
969    /**
970     * Authenticates the user using the XOAUTH2 method.
971     *
972     * @param string $uid   The userid to authenticate as.
973     * @param string $token The access token to authenticate with.
974     * @param string $authz The optional authorization proxy identifier.
975     *
976     * @return mixed Returns a PEAR_Error with an error message on any
977     *               kind of failure, or true on success.
978     * @since 1.9.0
979     */
980    public function authXOAuth2($uid, $token, $authz, $conn)
981    {
982        $auth = base64_encode("user=$uid\1auth=$token\1\1");
983        if (PEAR::isError($error = $this->put('AUTH', 'XOAUTH2 ' . $auth))) {
984            return $error;
985        }
986
987        /* 235: Authentication successful or 334: Continue authentication */
988        if (PEAR::isError($error = $this->parseResponse([235, 334]))) {
989            return $error;
990        }
991
992        /* 334: Continue authentication request */
993        if ($this->code === 334) {
994            /* Send an empty line as response to 334 */
995            if (PEAR::isError($error = $this->put(''))) {
996                return $error;
997            }
998
999            /* Expect 235: Authentication successful */
1000            if (PEAR::isError($error = $this->parseResponse(235))) {
1001                return $error;
1002            }
1003        }
1004
1005        return true;
1006    }
1007
1008    /**
1009     * Send the HELO command.
1010     *
1011     * @param string $domain The domain name to say we are.
1012     *
1013     * @return mixed Returns a PEAR_Error with an error message on any
1014     *               kind of failure, or true on success.
1015     * @since 1.0
1016     */
1017    public function helo($domain)
1018    {
1019        if (PEAR::isError($error = $this->put('HELO', $domain))) {
1020            return $error;
1021        }
1022        if (PEAR::isError($error = $this->parseResponse(250))) {
1023            return $error;
1024        }
1025
1026        return true;
1027    }
1028
1029    /**
1030     * Return the list of SMTP service extensions advertised by the server.
1031     *
1032     * @return array The list of SMTP service extensions.
1033     * @since 1.3
1034     */
1035    public function getServiceExtensions()
1036    {
1037        return $this->esmtp;
1038    }
1039
1040    /**
1041     * Send the MAIL FROM: command.
1042     *
1043     * @param string $sender The sender (reverse path) to set.
1044     * @param string $params String containing additional MAIL parameters,
1045     *                       such as the NOTIFY flags defined by RFC 1891
1046     *                       or the VERP protocol.
1047     *
1048     *                       If $params is an array, only the 'verp' option
1049     *                       is supported.  If 'verp' is true, the XVERP
1050     *                       parameter is appended to the MAIL command.
1051     *                       If the 'verp' value is a string, the full
1052     *                       XVERP=value parameter is appended.
1053     *
1054     * @return mixed Returns a PEAR_Error with an error message on any
1055     *               kind of failure, or true on success.
1056     * @since 1.0
1057     */
1058    public function mailFrom($sender, $params = null)
1059    {
1060        $args = "FROM:<$sender>";
1061
1062        /* Support the deprecated array form of $params. */
1063        if (is_array($params) && isset($params['verp'])) {
1064            if ($params['verp'] === true) {
1065                $args .= ' XVERP';
1066            } elseif (trim($params['verp'])) {
1067                $args .= ' XVERP=' . $params['verp'];
1068            }
1069        } elseif (is_string($params) && !empty($params)) {
1070            $args .= ' ' . $params;
1071        }
1072
1073        if (PEAR::isError($error = $this->put('MAIL', $args))) {
1074            return $error;
1075        }
1076        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1077            return $error;
1078        }
1079
1080        return true;
1081    }
1082
1083    /**
1084     * Send the RCPT TO: command.
1085     *
1086     * @param string $recipient The recipient (forward path) to add.
1087     * @param string $params    String containing additional RCPT parameters,
1088     *                          such as the NOTIFY flags defined by RFC 1891.
1089     *
1090     * @return mixed Returns a PEAR_Error with an error message on any
1091     *               kind of failure, or true on success.
1092     *
1093     * @since 1.0
1094     */
1095    public function rcptTo($recipient, $params = null)
1096    {
1097        $args = "TO:<$recipient>";
1098        if (is_string($params)) {
1099            $args .= ' ' . $params;
1100        }
1101
1102        if (PEAR::isError($error = $this->put('RCPT', $args))) {
1103            return $error;
1104        }
1105        if (PEAR::isError($error = $this->parseResponse(array(250, 251), $this->pipelining))) {
1106            return $error;
1107        }
1108
1109        return true;
1110    }
1111
1112    /**
1113     * Quote the data so that it meets SMTP standards.
1114     *
1115     * This is provided as a separate public function to facilitate
1116     * easier overloading for the cases where it is desirable to
1117     * customize the quoting behavior.
1118     *
1119     * @param string &$data The message text to quote. The string must be passed
1120     *                      by reference, and the text will be modified in place.
1121     *
1122     * @since 1.2
1123     */
1124    public function quotedata(&$data)
1125    {
1126        /* Because a single leading period (.) signifies an end to the
1127         * data, legitimate leading periods need to be "doubled" ('..'). */
1128        $data = preg_replace('/^\./m', '..', $data);
1129
1130        /* Change Unix (\n) and Mac (\r) linefeeds into CRLF's (\r\n). */
1131        $data = preg_replace('/(?:\r\n|\n|\r(?!\n))/', "\r\n", $data);
1132    }
1133
1134    /**
1135     * Send the DATA command.
1136     *
1137     * @param mixed  $data    The message data, either as a string or an open
1138     *                        file resource.
1139     * @param string $headers The message headers.  If $headers is provided,
1140     *                        $data is assumed to contain only body data.
1141     *
1142     * @return mixed Returns a PEAR_Error with an error message on any
1143     *               kind of failure, or true on success.
1144     * @since 1.0
1145     */
1146    public function data($data, $headers = null)
1147    {
1148        /* Verify that $data is a supported type. */
1149        if (!is_string($data) && !is_resource($data)) {
1150            return PEAR::raiseError('Expected a string or file resource');
1151        }
1152
1153        /* Start by considering the size of the optional headers string.  We
1154         * also account for the addition 4 character "\r\n\r\n" separator
1155         * sequence. */
1156        $size = $headers_size = (is_null($headers)) ? 0 : strlen($headers) + 4;
1157
1158        if (is_resource($data)) {
1159            $stat = fstat($data);
1160            if ($stat === false) {
1161                return PEAR::raiseError('Failed to get file size');
1162            }
1163            $size += $stat['size'];
1164        } else {
1165            $size += strlen($data);
1166        }
1167
1168        /* RFC 1870, section 3, subsection 3 states "a value of zero indicates
1169         * that no fixed maximum message size is in force".  Furthermore, it
1170         * says that if "the parameter is omitted no information is conveyed
1171         * about the server's fixed maximum message size". */
1172        $limit = (isset($this->esmtp['SIZE'])) ? $this->esmtp['SIZE'] : 0;
1173        if ($limit > 0 && $size >= $limit) {
1174            return PEAR::raiseError('Message size exceeds server limit');
1175        }
1176
1177        /* Initiate the DATA command. */
1178        if (PEAR::isError($error = $this->put('DATA'))) {
1179            return $error;
1180        }
1181        if (PEAR::isError($error = $this->parseResponse(354))) {
1182            return $error;
1183        }
1184
1185        /* If we have a separate headers string, send it first. */
1186        if (!is_null($headers)) {
1187            $this->quotedata($headers);
1188            if (PEAR::isError($result = $this->send($headers . "\r\n\r\n"))) {
1189                return $result;
1190            }
1191
1192            /* Subtract the headers size now that they've been sent. */
1193            $size -= $headers_size;
1194        }
1195
1196        /* Now we can send the message body data. */
1197        if (is_resource($data)) {
1198            /* Stream the contents of the file resource out over our socket
1199             * connection, line by line.  Each line must be run through the
1200             * quoting routine. */
1201            while (strlen($line = fread($data, 8192)) > 0) {
1202                /* If the last character is an newline, we need to grab the
1203                 * next character to check to see if it is a period. */
1204                while (!feof($data)) {
1205                    $char = fread($data, 1);
1206                    $line .= $char;
1207                    if ($char != "\n") {
1208                        break;
1209                    }
1210                }
1211                $this->quotedata($line);
1212                if (PEAR::isError($result = $this->send($line))) {
1213                    return $result;
1214                }
1215            }
1216
1217             $last = $line;
1218        } else {
1219            /*
1220             * Break up the data by sending one chunk (up to 512k) at a time.
1221             * This approach reduces our peak memory usage.
1222             */
1223            for ($offset = 0; $offset < $size;) {
1224                $end = $offset + 512000;
1225
1226                /*
1227                 * Ensure we don't read beyond our data size or span multiple
1228                 * lines.  quotedata() can't properly handle character data
1229                 * that's split across two line break boundaries.
1230                 */
1231                if ($end >= $size) {
1232                    $end = $size;
1233                } else {
1234                    for (; $end < $size; $end++) {
1235                        if ($data[$end] != "\n") {
1236                            break;
1237                        }
1238                    }
1239                }
1240
1241                /* Extract our chunk and run it through the quoting routine. */
1242                $chunk = substr($data, $offset, $end - $offset);
1243                $this->quotedata($chunk);
1244
1245                /* If we run into a problem along the way, abort. */
1246                if (PEAR::isError($result = $this->send($chunk))) {
1247                    return $result;
1248                }
1249
1250                /* Advance the offset to the end of this chunk. */
1251                $offset = $end;
1252            }
1253
1254            $last = $chunk;
1255        }
1256
1257        /* Don't add another CRLF sequence if it's already in the data */
1258        $terminator = (substr($last, -2) == "\r\n" ? '' : "\r\n") . ".\r\n";
1259
1260        /* Finally, send the DATA terminator sequence. */
1261        if (PEAR::isError($result = $this->send($terminator))) {
1262            return $result;
1263        }
1264
1265        /* Verify that the data was successfully received by the server. */
1266        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1267            return $error;
1268        }
1269
1270        return true;
1271    }
1272
1273    /**
1274     * Send the SEND FROM: command.
1275     *
1276     * @param string $path The reverse path to send.
1277     *
1278     * @return mixed Returns a PEAR_Error with an error message on any
1279     *               kind of failure, or true on success.
1280     * @since 1.2.6
1281     */
1282    public function sendFrom($path)
1283    {
1284        if (PEAR::isError($error = $this->put('SEND', "FROM:<$path>"))) {
1285            return $error;
1286        }
1287        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1288            return $error;
1289        }
1290
1291        return true;
1292    }
1293
1294    /**
1295     * Send the SOML FROM: command.
1296     *
1297     * @param string $path The reverse path to send.
1298     *
1299     * @return mixed Returns a PEAR_Error with an error message on any
1300     *               kind of failure, or true on success.
1301     * @since 1.2.6
1302     */
1303    public function somlFrom($path)
1304    {
1305        if (PEAR::isError($error = $this->put('SOML', "FROM:<$path>"))) {
1306            return $error;
1307        }
1308        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1309            return $error;
1310        }
1311
1312        return true;
1313    }
1314
1315    /**
1316     * Send the SAML FROM: command.
1317     *
1318     * @param string $path The reverse path to send.
1319     *
1320     * @return mixed Returns a PEAR_Error with an error message on any
1321     *               kind of failure, or true on success.
1322     * @since 1.2.6
1323     */
1324    public function samlFrom($path)
1325    {
1326        if (PEAR::isError($error = $this->put('SAML', "FROM:<$path>"))) {
1327            return $error;
1328        }
1329        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1330            return $error;
1331        }
1332
1333        return true;
1334    }
1335
1336    /**
1337     * Send the RSET command.
1338     *
1339     * @return mixed Returns a PEAR_Error with an error message on any
1340     *               kind of failure, or true on success.
1341     * @since  1.0
1342     */
1343    public function rset()
1344    {
1345        if (PEAR::isError($error = $this->put('RSET'))) {
1346            return $error;
1347        }
1348        if (PEAR::isError($error = $this->parseResponse(250, $this->pipelining))) {
1349            return $error;
1350        }
1351
1352        return true;
1353    }
1354
1355    /**
1356     * Send the VRFY command.
1357     *
1358     * @param string $string The string to verify
1359     *
1360     * @return mixed Returns a PEAR_Error with an error message on any
1361     *               kind of failure, or true on success.
1362     * @since 1.0
1363     */
1364    public function vrfy($string)
1365    {
1366        /* Note: 251 is also a valid response code */
1367        if (PEAR::isError($error = $this->put('VRFY', $string))) {
1368            return $error;
1369        }
1370        if (PEAR::isError($error = $this->parseResponse(array(250, 252)))) {
1371            return $error;
1372        }
1373
1374        return true;
1375    }
1376
1377    /**
1378     * Send the NOOP command.
1379     *
1380     * @return mixed Returns a PEAR_Error with an error message on any
1381     *               kind of failure, or true on success.
1382     * @since 1.0
1383     */
1384    public function noop()
1385    {
1386        if (PEAR::isError($error = $this->put('NOOP'))) {
1387            return $error;
1388        }
1389        if (PEAR::isError($error = $this->parseResponse(250))) {
1390            return $error;
1391        }
1392
1393        return true;
1394    }
1395
1396    /**
1397     * Backwards-compatibility method.  identifySender()'s functionality is
1398     * now handled internally.
1399     *
1400     * @return boolean This method always return true.
1401     *
1402     * @since 1.0
1403     */
1404    public function identifySender()
1405    {
1406        return true;
1407    }
1408}
1409