source: spip-zone/_plugins_/facteur/trunk/phpmailer-php5/class.smtp.php @ 92830

Last change on this file since 92830 was 92830, checked in by rastapopoulos@…, 4 years ago

Mettons à jour PHPMailer, qui a eu 4 versions de plus, dont de grosses choses, au moins : ajout de la méthode Oauth pour Gmail (qui a l'air compliqué cela dit, et pas encore intégré dans la config de Facteur !) + de la sécurité importante.

  • Property svn:executable set to *
File size: 39.0 KB
Line 
1<?php
2/**
3 * PHPMailer RFC821 SMTP email transport class.
4 * PHP Version 5
5 * @package PHPMailer
6 * @link https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
7 * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
8 * @author Jim Jagielski (jimjag) <jimjag@gmail.com>
9 * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
10 * @author Brent R. Matzelle (original founder)
11 * @copyright 2014 Marcus Bointon
12 * @copyright 2010 - 2012 Jim Jagielski
13 * @copyright 2004 - 2009 Andy Prevost
14 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
15 * @note This program is distributed in the hope that it will be useful - WITHOUT
16 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17 * FITNESS FOR A PARTICULAR PURPOSE.
18 */
19
20/**
21 * PHPMailer RFC821 SMTP email transport class.
22 * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
23 * @package PHPMailer
24 * @author Chris Ryan
25 * @author Marcus Bointon <phpmailer@synchromedia.co.uk>
26 */
27class SMTP
28{
29    /**
30     * The PHPMailer SMTP version number.
31     * @var string
32     */
33    const VERSION = '5.2.14';
34
35    /**
36     * SMTP line break constant.
37     * @var string
38     */
39    const CRLF = "\r\n";
40
41    /**
42     * The SMTP port to use if one is not specified.
43     * @var integer
44     */
45    const DEFAULT_SMTP_PORT = 25;
46
47    /**
48     * The maximum line length allowed by RFC 2822 section 2.1.1
49     * @var integer
50     */
51    const MAX_LINE_LENGTH = 998;
52
53    /**
54     * Debug level for no output
55     */
56    const DEBUG_OFF = 0;
57
58    /**
59     * Debug level to show client -> server messages
60     */
61    const DEBUG_CLIENT = 1;
62
63    /**
64     * Debug level to show client -> server and server -> client messages
65     */
66    const DEBUG_SERVER = 2;
67
68    /**
69     * Debug level to show connection status, client -> server and server -> client messages
70     */
71    const DEBUG_CONNECTION = 3;
72
73    /**
74     * Debug level to show all messages
75     */
76    const DEBUG_LOWLEVEL = 4;
77
78    /**
79     * The PHPMailer SMTP Version number.
80     * @var string
81     * @deprecated Use the `VERSION` constant instead
82     * @see SMTP::VERSION
83     */
84    public $Version = '5.2.14';
85
86    /**
87     * SMTP server port number.
88     * @var integer
89     * @deprecated This is only ever used as a default value, so use the `DEFAULT_SMTP_PORT` constant instead
90     * @see SMTP::DEFAULT_SMTP_PORT
91     */
92    public $SMTP_PORT = 25;
93
94    /**
95     * SMTP reply line ending.
96     * @var string
97     * @deprecated Use the `CRLF` constant instead
98     * @see SMTP::CRLF
99     */
100    public $CRLF = "\r\n";
101
102    /**
103     * Debug output level.
104     * Options:
105     * * self::DEBUG_OFF (`0`) No debug output, default
106     * * self::DEBUG_CLIENT (`1`) Client commands
107     * * self::DEBUG_SERVER (`2`) Client commands and server responses
108     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
109     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages
110     * @var integer
111     */
112    public $do_debug = self::DEBUG_OFF;
113
114    /**
115     * How to handle debug output.
116     * Options:
117     * * `echo` Output plain-text as-is, appropriate for CLI
118     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
119     * * `error_log` Output to error log as configured in php.ini
120     *
121     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
122     * <code>
123     * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
124     * </code>
125     * @var string|callable
126     */
127    public $Debugoutput = 'echo';
128
129    /**
130     * Whether to use VERP.
131     * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path
132     * @link http://www.postfix.org/VERP_README.html Info on VERP
133     * @var boolean
134     */
135    public $do_verp = false;
136
137    /**
138     * The timeout value for connection, in seconds.
139     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
140     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
141     * @link http://tools.ietf.org/html/rfc2821#section-4.5.3.2
142     * @var integer
143     */
144    public $Timeout = 300;
145
146    /**
147     * How long to wait for commands to complete, in seconds.
148     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
149     * @var integer
150     */
151    public $Timelimit = 300;
152
153    /**
154     * The socket for the server connection.
155     * @var resource
156     */
157    protected $smtp_conn;
158
159    /**
160     * Error information, if any, for the last SMTP command.
161     * @var array
162     */
163    protected $error = array(
164        'error' => '',
165        'detail' => '',
166        'smtp_code' => '',
167        'smtp_code_ex' => ''
168    );
169
170    /**
171     * The reply the server sent to us for HELO.
172     * If null, no HELO string has yet been received.
173     * @var string|null
174     */
175    protected $helo_rply = null;
176
177    /**
178     * The set of SMTP extensions sent in reply to EHLO command.
179     * Indexes of the array are extension names.
180     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
181     * represents the server name. In case of HELO it is the only element of the array.
182     * Other values can be boolean TRUE or an array containing extension options.
183     * If null, no HELO/EHLO string has yet been received.
184     * @var array|null
185     */
186    protected $server_caps = null;
187
188    /**
189     * The most recent reply received from the server.
190     * @var string
191     */
192    protected $last_reply = '';
193
194    /**
195     * Output debugging info via a user-selected method.
196     * @see SMTP::$Debugoutput
197     * @see SMTP::$do_debug
198     * @param string $str Debug string to output
199     * @param integer $level The debug level of this message; see DEBUG_* constants
200     * @return void
201     */
202    protected function edebug($str, $level = 0)
203    {
204        if ($level > $this->do_debug) {
205            return;
206        }
207        //Avoid clash with built-in function names
208        if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) {
209            call_user_func($this->Debugoutput, $str, $this->do_debug);
210            return;
211        }
212        switch ($this->Debugoutput) {
213            case 'error_log':
214                //Don't output, just log
215                error_log($str);
216                break;
217            case 'html':
218                //Cleans up output a bit for a better looking, HTML-safe output
219                echo htmlentities(
220                    preg_replace('/[\r\n]+/', '', $str),
221                    ENT_QUOTES,
222                    'UTF-8'
223                )
224                . "<br>\n";
225                break;
226            case 'echo':
227            default:
228                //Normalize line breaks
229                $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str);
230                echo gmdate('Y-m-d H:i:s') . "\t" . str_replace(
231                    "\n",
232                    "\n                   \t                  ",
233                    trim($str)
234                )."\n";
235        }
236    }
237
238    /**
239     * Connect to an SMTP server.
240     * @param string $host SMTP server IP or host name
241     * @param integer $port The port number to connect to
242     * @param integer $timeout How long to wait for the connection to open
243     * @param array $options An array of options for stream_context_create()
244     * @access public
245     * @return boolean
246     */
247    public function connect($host, $port = null, $timeout = 30, $options = array())
248    {
249        static $streamok;
250        //This is enabled by default since 5.0.0 but some providers disable it
251        //Check this once and cache the result
252        if (is_null($streamok)) {
253            $streamok = function_exists('stream_socket_client');
254        }
255        // Clear errors to avoid confusion
256        $this->setError('');
257        // Make sure we are __not__ connected
258        if ($this->connected()) {
259            // Already connected, generate error
260            $this->setError('Already connected to a server');
261            return false;
262        }
263        if (empty($port)) {
264            $port = self::DEFAULT_SMTP_PORT;
265        }
266        // Connect to the SMTP server
267        $this->edebug(
268            "Connection: opening to $host:$port, timeout=$timeout, options=".var_export($options, true),
269            self::DEBUG_CONNECTION
270        );
271        $errno = 0;
272        $errstr = '';
273        if ($streamok) {
274            $socket_context = stream_context_create($options);
275            //Suppress errors; connection failures are handled at a higher level
276            $this->smtp_conn = @stream_socket_client(
277                $host . ":" . $port,
278                $errno,
279                $errstr,
280                $timeout,
281                STREAM_CLIENT_CONNECT,
282                $socket_context
283            );
284        } else {
285            //Fall back to fsockopen which should work in more places, but is missing some features
286            $this->edebug(
287                "Connection: stream_socket_client not available, falling back to fsockopen",
288                self::DEBUG_CONNECTION
289            );
290            $this->smtp_conn = fsockopen(
291                $host,
292                $port,
293                $errno,
294                $errstr,
295                $timeout
296            );
297        }
298        // Verify we connected properly
299        if (!is_resource($this->smtp_conn)) {
300            $this->setError(
301                'Failed to connect to server',
302                $errno,
303                $errstr
304            );
305            $this->edebug(
306                'SMTP ERROR: ' . $this->error['error']
307                . ": $errstr ($errno)",
308                self::DEBUG_CLIENT
309            );
310            return false;
311        }
312        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
313        // SMTP server can take longer to respond, give longer timeout for first read
314        // Windows does not have support for this timeout function
315        if (substr(PHP_OS, 0, 3) != 'WIN') {
316            $max = ini_get('max_execution_time');
317            // Don't bother if unlimited
318            if ($max != 0 && $timeout > $max) {
319                @set_time_limit($timeout);
320            }
321            stream_set_timeout($this->smtp_conn, $timeout, 0);
322        }
323        // Get any announcement
324        $announce = $this->get_lines();
325        $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
326        return true;
327    }
328
329    /**
330     * Initiate a TLS (encrypted) session.
331     * @access public
332     * @return boolean
333     */
334    public function startTLS()
335    {
336        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
337            return false;
338        }
339        // Begin encrypted connection
340        if (!stream_socket_enable_crypto(
341            $this->smtp_conn,
342            true,
343            STREAM_CRYPTO_METHOD_TLS_CLIENT
344        )) {
345            return false;
346        }
347        return true;
348    }
349
350    /**
351     * Perform SMTP authentication.
352     * Must be run after hello().
353     * @see hello()
354     * @param string $username The user name
355     * @param string $password The password
356     * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5, XOAUTH2)
357     * @param string $realm The auth realm for NTLM
358     * @param string $workstation The auth workstation for NTLM
359     * @param null|OAuth $OAuth An optional OAuth instance (@see PHPMailerOAuth)
360     * @return bool True if successfully authenticated.* @access public
361     */
362    public function authenticate(
363        $username,
364        $password,
365        $authtype = null,
366        $realm = '',
367        $workstation = '',
368        $OAuth = null
369    ) {
370        if (!$this->server_caps) {
371            $this->setError('Authentication is not allowed before HELO/EHLO');
372            return false;
373        }
374
375        if (array_key_exists('EHLO', $this->server_caps)) {
376        // SMTP extensions are available. Let's try to find a proper authentication method
377
378            if (!array_key_exists('AUTH', $this->server_caps)) {
379                $this->setError('Authentication is not allowed at this stage');
380                // 'at this stage' means that auth may be allowed after the stage changes
381                // e.g. after STARTTLS
382                return false;
383            }
384
385            self::edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL);
386            self::edebug(
387                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
388                self::DEBUG_LOWLEVEL
389            );
390
391            if (empty($authtype)) {
392                foreach (array('LOGIN', 'CRAM-MD5', 'NTLM', 'PLAIN', 'XOAUTH2') as $method) {
393                    if (in_array($method, $this->server_caps['AUTH'])) {
394                        $authtype = $method;
395                        break;
396                    }
397                }
398                if (empty($authtype)) {
399                    $this->setError('No supported authentication methods found');
400                    return false;
401                }
402                self::edebug('Auth method selected: '.$authtype, self::DEBUG_LOWLEVEL);
403            }
404
405            if (!in_array($authtype, $this->server_caps['AUTH'])) {
406                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
407                return false;
408            }
409        } elseif (empty($authtype)) {
410            $authtype = 'LOGIN';
411        }
412        switch ($authtype) {
413            case 'PLAIN':
414                // Start authentication
415                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
416                    return false;
417                }
418                // Send encoded username and password
419                if (!$this->sendCommand(
420                    'User & Password',
421                    base64_encode("\0" . $username . "\0" . $password),
422                    235
423                )
424                ) {
425                    return false;
426                }
427                break;
428            case 'LOGIN':
429                // Start authentication
430                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
431                    return false;
432                }
433                if (!$this->sendCommand("Username", base64_encode($username), 334)) {
434                    return false;
435                }
436                if (!$this->sendCommand("Password", base64_encode($password), 235)) {
437                    return false;
438                }
439                break;
440            case 'XOAUTH2':
441                //If the OAuth Instance is not set. Can be a case when PHPMailer is used
442                //instead of PHPMailerOAuth
443                if (is_null($OAuth)) {
444                    return false;
445                }
446                $oauth = $OAuth->getOauth64();
447
448                // Start authentication
449                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
450                    return false;
451                }
452                break;
453            case 'NTLM':
454                /*
455                 * ntlm_sasl_client.php
456                 * Bundled with Permission
457                 *
458                 * How to telnet in windows:
459                 * http://technet.microsoft.com/en-us/library/aa995718%28EXCHG.65%29.aspx
460                 * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication
461                 */
462                require_once 'extras/ntlm_sasl_client.php';
463                $temp = new stdClass;
464                $ntlm_client = new ntlm_sasl_client_class;
465                //Check that functions are available
466                if (!$ntlm_client->Initialize($temp)) {
467                    $this->setError($temp->error);
468                    $this->edebug(
469                        'You need to enable some modules in your php.ini file: '
470                        . $this->error['error'],
471                        self::DEBUG_CLIENT
472                    );
473                    return false;
474                }
475                //msg1
476                $msg1 = $ntlm_client->TypeMsg1($realm, $workstation); //msg1
477
478                if (!$this->sendCommand(
479                    'AUTH NTLM',
480                    'AUTH NTLM ' . base64_encode($msg1),
481                    334
482                )
483                ) {
484                    return false;
485                }
486                //Though 0 based, there is a white space after the 3 digit number
487                //msg2
488                $challenge = substr($this->last_reply, 3);
489                $challenge = base64_decode($challenge);
490                $ntlm_res = $ntlm_client->NTLMResponse(
491                    substr($challenge, 24, 8),
492                    $password
493                );
494                //msg3
495                $msg3 = $ntlm_client->TypeMsg3(
496                    $ntlm_res,
497                    $username,
498                    $realm,
499                    $workstation
500                );
501                // send encoded username
502                return $this->sendCommand('Username', base64_encode($msg3), 235);
503            case 'CRAM-MD5':
504                // Start authentication
505                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
506                    return false;
507                }
508                // Get the challenge
509                $challenge = base64_decode(substr($this->last_reply, 4));
510
511                // Build the response
512                $response = $username . ' ' . $this->hmac($challenge, $password);
513
514                // send encoded credentials
515                return $this->sendCommand('Username', base64_encode($response), 235);
516            default:
517                $this->setError("Authentication method \"$authtype\" is not supported");
518                return false;
519        }
520        return true;
521    }
522
523    /**
524     * Calculate an MD5 HMAC hash.
525     * Works like hash_hmac('md5', $data, $key)
526     * in case that function is not available
527     * @param string $data The data to hash
528     * @param string $key  The key to hash with
529     * @access protected
530     * @return string
531     */
532    protected function hmac($data, $key)
533    {
534        if (function_exists('hash_hmac')) {
535            return hash_hmac('md5', $data, $key);
536        }
537
538        // The following borrowed from
539        // http://php.net/manual/en/function.mhash.php#27225
540
541        // RFC 2104 HMAC implementation for php.
542        // Creates an md5 HMAC.
543        // Eliminates the need to install mhash to compute a HMAC
544        // by Lance Rushing
545
546        $bytelen = 64; // byte length for md5
547        if (strlen($key) > $bytelen) {
548            $key = pack('H*', md5($key));
549        }
550        $key = str_pad($key, $bytelen, chr(0x00));
551        $ipad = str_pad('', $bytelen, chr(0x36));
552        $opad = str_pad('', $bytelen, chr(0x5c));
553        $k_ipad = $key ^ $ipad;
554        $k_opad = $key ^ $opad;
555
556        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
557    }
558
559    /**
560     * Check connection state.
561     * @access public
562     * @return boolean True if connected.
563     */
564    public function connected()
565    {
566        if (is_resource($this->smtp_conn)) {
567            $sock_status = stream_get_meta_data($this->smtp_conn);
568            if ($sock_status['eof']) {
569                // The socket is valid but we are not connected
570                $this->edebug(
571                    'SMTP NOTICE: EOF caught while checking if connected',
572                    self::DEBUG_CLIENT
573                );
574                $this->close();
575                return false;
576            }
577            return true; // everything looks good
578        }
579        return false;
580    }
581
582    /**
583     * Close the socket and clean up the state of the class.
584     * Don't use this function without first trying to use QUIT.
585     * @see quit()
586     * @access public
587     * @return void
588     */
589    public function close()
590    {
591        $this->setError('');
592        $this->server_caps = null;
593        $this->helo_rply = null;
594        if (is_resource($this->smtp_conn)) {
595            // close the connection and cleanup
596            fclose($this->smtp_conn);
597            $this->smtp_conn = null; //Makes for cleaner serialization
598            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
599        }
600    }
601
602    /**
603     * Send an SMTP DATA command.
604     * Issues a data command and sends the msg_data to the server,
605     * finializing the mail transaction. $msg_data is the message
606     * that is to be send with the headers. Each header needs to be
607     * on a single line followed by a <CRLF> with the message headers
608     * and the message body being separated by and additional <CRLF>.
609     * Implements rfc 821: DATA <CRLF>
610     * @param string $msg_data Message data to send
611     * @access public
612     * @return boolean
613     */
614    public function data($msg_data)
615    {
616        //This will use the standard timelimit
617        if (!$this->sendCommand('DATA', 'DATA', 354)) {
618            return false;
619        }
620
621        /* The server is ready to accept data!
622         * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF)
623         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
624         * smaller lines to fit within the limit.
625         * We will also look for lines that start with a '.' and prepend an additional '.'.
626         * NOTE: this does not count towards line-length limit.
627         */
628
629        // Normalize line breaks before exploding
630        $lines = explode("\n", str_replace(array("\r\n", "\r"), "\n", $msg_data));
631
632        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
633         * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
634         * process all lines before a blank line as headers.
635         */
636
637        $field = substr($lines[0], 0, strpos($lines[0], ':'));
638        $in_headers = false;
639        if (!empty($field) && strpos($field, ' ') === false) {
640            $in_headers = true;
641        }
642
643        foreach ($lines as $line) {
644            $lines_out = array();
645            if ($in_headers and $line == '') {
646                $in_headers = false;
647            }
648            //Break this line up into several smaller lines if it's too long
649            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
650            while (isset($line[self::MAX_LINE_LENGTH])) {
651                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
652                //so as to avoid breaking in the middle of a word
653                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
654                //Deliberately matches both false and 0
655                if (!$pos) {
656                    //No nice break found, add a hard break
657                    $pos = self::MAX_LINE_LENGTH - 1;
658                    $lines_out[] = substr($line, 0, $pos);
659                    $line = substr($line, $pos);
660                } else {
661                    //Break at the found point
662                    $lines_out[] = substr($line, 0, $pos);
663                    //Move along by the amount we dealt with
664                    $line = substr($line, $pos + 1);
665                }
666                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
667                if ($in_headers) {
668                    $line = "\t" . $line;
669                }
670            }
671            $lines_out[] = $line;
672
673            //Send the lines to the server
674            foreach ($lines_out as $line_out) {
675                //RFC2821 section 4.5.2
676                if (!empty($line_out) and $line_out[0] == '.') {
677                    $line_out = '.' . $line_out;
678                }
679                $this->client_send($line_out . self::CRLF);
680            }
681        }
682
683        //Message data has been sent, complete the command
684        //Increase timelimit for end of DATA command
685        $savetimelimit = $this->Timelimit;
686        $this->Timelimit = $this->Timelimit * 2;
687        $result = $this->sendCommand('DATA END', '.', 250);
688        //Restore timelimit
689        $this->Timelimit = $savetimelimit;
690        return $result;
691    }
692
693    /**
694     * Send an SMTP HELO or EHLO command.
695     * Used to identify the sending server to the receiving server.
696     * This makes sure that client and server are in a known state.
697     * Implements RFC 821: HELO <SP> <domain> <CRLF>
698     * and RFC 2821 EHLO.
699     * @param string $host The host name or IP to connect to
700     * @access public
701     * @return boolean
702     */
703    public function hello($host = '')
704    {
705        //Try extended hello first (RFC 2821)
706        return (boolean)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host));
707    }
708
709    /**
710     * Send an SMTP HELO or EHLO command.
711     * Low-level implementation used by hello()
712     * @see hello()
713     * @param string $hello The HELO string
714     * @param string $host The hostname to say we are
715     * @access protected
716     * @return boolean
717     */
718    protected function sendHello($hello, $host)
719    {
720        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
721        $this->helo_rply = $this->last_reply;
722        if ($noerror) {
723            $this->parseHelloFields($hello);
724        } else {
725            $this->server_caps = null;
726        }
727        return $noerror;
728    }
729
730    /**
731     * Parse a reply to HELO/EHLO command to discover server extensions.
732     * In case of HELO, the only parameter that can be discovered is a server name.
733     * @access protected
734     * @param string $type - 'HELO' or 'EHLO'
735     */
736    protected function parseHelloFields($type)
737    {
738        $this->server_caps = array();
739        $lines = explode("\n", $this->last_reply);
740
741        foreach ($lines as $n => $s) {
742            //First 4 chars contain response code followed by - or space
743            $s = trim(substr($s, 4));
744            if (empty($s)) {
745                continue;
746            }
747            $fields = explode(' ', $s);
748            if (!empty($fields)) {
749                if (!$n) {
750                    $name = $type;
751                    $fields = $fields[0];
752                } else {
753                    $name = array_shift($fields);
754                    switch ($name) {
755                        case 'SIZE':
756                            $fields = ($fields ? $fields[0] : 0);
757                            break;
758                        case 'AUTH':
759                            if (!is_array($fields)) {
760                                $fields = array();
761                            }
762                            break;
763                        default:
764                            $fields = true;
765                    }
766                }
767                $this->server_caps[$name] = $fields;
768            }
769        }
770    }
771
772    /**
773     * Send an SMTP MAIL command.
774     * Starts a mail transaction from the email address specified in
775     * $from. Returns true if successful or false otherwise. If True
776     * the mail transaction is started and then one or more recipient
777     * commands may be called followed by a data command.
778     * Implements rfc 821: MAIL <SP> FROM:<reverse-path> <CRLF>
779     * @param string $from Source address of this message
780     * @access public
781     * @return boolean
782     */
783    public function mail($from)
784    {
785        $useVerp = ($this->do_verp ? ' XVERP' : '');
786        return $this->sendCommand(
787            'MAIL FROM',
788            'MAIL FROM:<' . $from . '>' . $useVerp,
789            250
790        );
791    }
792
793    /**
794     * Send an SMTP QUIT command.
795     * Closes the socket if there is no error or the $close_on_error argument is true.
796     * Implements from rfc 821: QUIT <CRLF>
797     * @param boolean $close_on_error Should the connection close if an error occurs?
798     * @access public
799     * @return boolean
800     */
801    public function quit($close_on_error = true)
802    {
803        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
804        $err = $this->error; //Save any error
805        if ($noerror or $close_on_error) {
806            $this->close();
807            $this->error = $err; //Restore any error from the quit command
808        }
809        return $noerror;
810    }
811
812    /**
813     * Send an SMTP RCPT command.
814     * Sets the TO argument to $toaddr.
815     * Returns true if the recipient was accepted false if it was rejected.
816     * Implements from rfc 821: RCPT <SP> TO:<forward-path> <CRLF>
817     * @param string $address The address the message is being sent to
818     * @access public
819     * @return boolean
820     */
821    public function recipient($address)
822    {
823        return $this->sendCommand(
824            'RCPT TO',
825            'RCPT TO:<' . $address . '>',
826            array(250, 251)
827        );
828    }
829
830    /**
831     * Send an SMTP RSET command.
832     * Abort any transaction that is currently in progress.
833     * Implements rfc 821: RSET <CRLF>
834     * @access public
835     * @return boolean True on success.
836     */
837    public function reset()
838    {
839        return $this->sendCommand('RSET', 'RSET', 250);
840    }
841
842    /**
843     * Send a command to an SMTP server and check its return code.
844     * @param string $command The command name - not sent to the server
845     * @param string $commandstring The actual command to send
846     * @param integer|array $expect One or more expected integer success codes
847     * @access protected
848     * @return boolean True on success.
849     */
850    protected function sendCommand($command, $commandstring, $expect)
851    {
852        if (!$this->connected()) {
853            $this->setError("Called $command without being connected");
854            return false;
855        }
856        //Reject line breaks in all commands
857        if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) {
858            $this->setError("Command '$command' contained line breaks");
859            return false;
860        }
861        $this->client_send($commandstring . self::CRLF);
862
863        $this->last_reply = $this->get_lines();
864        // Fetch SMTP code and possible error code explanation
865        $matches = array();
866        if (preg_match("/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches)) {
867            $code = $matches[1];
868            $code_ex = (count($matches) > 2 ? $matches[2] : null);
869            // Cut off error code from each response line
870            $detail = preg_replace(
871                "/{$code}[ -]".($code_ex ? str_replace('.', '\\.', $code_ex).' ' : '')."/m",
872                '',
873                $this->last_reply
874            );
875        } else {
876            // Fall back to simple parsing if regex fails
877            $code = substr($this->last_reply, 0, 3);
878            $code_ex = null;
879            $detail = substr($this->last_reply, 4);
880        }
881
882        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
883
884        if (!in_array($code, (array)$expect)) {
885            $this->setError(
886                "$command command failed",
887                $detail,
888                $code,
889                $code_ex
890            );
891            $this->edebug(
892                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
893                self::DEBUG_CLIENT
894            );
895            return false;
896        }
897
898        $this->setError('');
899        return true;
900    }
901
902    /**
903     * Send an SMTP SAML command.
904     * Starts a mail transaction from the email address specified in $from.
905     * Returns true if successful or false otherwise. If True
906     * the mail transaction is started and then one or more recipient
907     * commands may be called followed by a data command. This command
908     * will send the message to the users terminal if they are logged
909     * in and send them an email.
910     * Implements rfc 821: SAML <SP> FROM:<reverse-path> <CRLF>
911     * @param string $from The address the message is from
912     * @access public
913     * @return boolean
914     */
915    public function sendAndMail($from)
916    {
917        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
918    }
919
920    /**
921     * Send an SMTP VRFY command.
922     * @param string $name The name to verify
923     * @access public
924     * @return boolean
925     */
926    public function verify($name)
927    {
928        return $this->sendCommand('VRFY', "VRFY $name", array(250, 251));
929    }
930
931    /**
932     * Send an SMTP NOOP command.
933     * Used to keep keep-alives alive, doesn't actually do anything
934     * @access public
935     * @return boolean
936     */
937    public function noop()
938    {
939        return $this->sendCommand('NOOP', 'NOOP', 250);
940    }
941
942    /**
943     * Send an SMTP TURN command.
944     * This is an optional command for SMTP that this class does not support.
945     * This method is here to make the RFC821 Definition complete for this class
946     * and _may_ be implemented in future
947     * Implements from rfc 821: TURN <CRLF>
948     * @access public
949     * @return boolean
950     */
951    public function turn()
952    {
953        $this->setError('The SMTP TURN command is not implemented');
954        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
955        return false;
956    }
957
958    /**
959     * Send raw data to the server.
960     * @param string $data The data to send
961     * @access public
962     * @return integer|boolean The number of bytes sent to the server or false on error
963     */
964    public function client_send($data)
965    {
966        $this->edebug("CLIENT -> SERVER: $data", self::DEBUG_CLIENT);
967        return fwrite($this->smtp_conn, $data);
968    }
969
970    /**
971     * Get the latest error.
972     * @access public
973     * @return array
974     */
975    public function getError()
976    {
977        return $this->error;
978    }
979
980    /**
981     * Get SMTP extensions available on the server
982     * @access public
983     * @return array|null
984     */
985    public function getServerExtList()
986    {
987        return $this->server_caps;
988    }
989
990    /**
991     * A multipurpose method
992     * The method works in three ways, dependent on argument value and current state
993     *   1. HELO/EHLO was not sent - returns null and set up $this->error
994     *   2. HELO was sent
995     *     $name = 'HELO': returns server name
996     *     $name = 'EHLO': returns boolean false
997     *     $name = any string: returns null and set up $this->error
998     *   3. EHLO was sent
999     *     $name = 'HELO'|'EHLO': returns server name
1000     *     $name = any string: if extension $name exists, returns boolean True
1001     *       or its options. Otherwise returns boolean False
1002     * In other words, one can use this method to detect 3 conditions:
1003     *  - null returned: handshake was not or we don't know about ext (refer to $this->error)
1004     *  - false returned: the requested feature exactly not exists
1005     *  - positive value returned: the requested feature exists
1006     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
1007     * @return mixed
1008     */
1009    public function getServerExt($name)
1010    {
1011        if (!$this->server_caps) {
1012            $this->setError('No HELO/EHLO was sent');
1013            return null;
1014        }
1015
1016        // the tight logic knot ;)
1017        if (!array_key_exists($name, $this->server_caps)) {
1018            if ($name == 'HELO') {
1019                return $this->server_caps['EHLO'];
1020            }
1021            if ($name == 'EHLO' || array_key_exists('EHLO', $this->server_caps)) {
1022                return false;
1023            }
1024            $this->setError('HELO handshake was used. Client knows nothing about server extensions');
1025            return null;
1026        }
1027
1028        return $this->server_caps[$name];
1029    }
1030
1031    /**
1032     * Get the last reply from the server.
1033     * @access public
1034     * @return string
1035     */
1036    public function getLastReply()
1037    {
1038        return $this->last_reply;
1039    }
1040
1041    /**
1042     * Read the SMTP server's response.
1043     * Either before eof or socket timeout occurs on the operation.
1044     * With SMTP we can tell if we have more lines to read if the
1045     * 4th character is '-' symbol. If it is a space then we don't
1046     * need to read anything else.
1047     * @access protected
1048     * @return string
1049     */
1050    protected function get_lines()
1051    {
1052        // If the connection is bad, give up straight away
1053        if (!is_resource($this->smtp_conn)) {
1054            return '';
1055        }
1056        $data = '';
1057        $endtime = 0;
1058        stream_set_timeout($this->smtp_conn, $this->Timeout);
1059        if ($this->Timelimit > 0) {
1060            $endtime = time() + $this->Timelimit;
1061        }
1062        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1063            $str = @fgets($this->smtp_conn, 515);
1064            $this->edebug("SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL);
1065            $this->edebug("SMTP -> get_lines(): \$str is  \"$str\"", self::DEBUG_LOWLEVEL);
1066            $data .= $str;
1067            // If 4th character is a space, we are done reading, break the loop, micro-optimisation over strlen
1068            if ((isset($str[3]) and $str[3] == ' ')) {
1069                break;
1070            }
1071            // Timed-out? Log and break
1072            $info = stream_get_meta_data($this->smtp_conn);
1073            if ($info['timed_out']) {
1074                $this->edebug(
1075                    'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)',
1076                    self::DEBUG_LOWLEVEL
1077                );
1078                break;
1079            }
1080            // Now check if reads took too long
1081            if ($endtime and time() > $endtime) {
1082                $this->edebug(
1083                    'SMTP -> get_lines(): timelimit reached ('.
1084                    $this->Timelimit . ' sec)',
1085                    self::DEBUG_LOWLEVEL
1086                );
1087                break;
1088            }
1089        }
1090        return $data;
1091    }
1092
1093    /**
1094     * Enable or disable VERP address generation.
1095     * @param boolean $enabled
1096     */
1097    public function setVerp($enabled = false)
1098    {
1099        $this->do_verp = $enabled;
1100    }
1101
1102    /**
1103     * Get VERP address generation mode.
1104     * @return boolean
1105     */
1106    public function getVerp()
1107    {
1108        return $this->do_verp;
1109    }
1110
1111    /**
1112     * Set error messages and codes.
1113     * @param string $message The error message
1114     * @param string $detail Further detail on the error
1115     * @param string $smtp_code An associated SMTP error code
1116     * @param string $smtp_code_ex Extended SMTP code
1117     */
1118    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1119    {
1120        $this->error = array(
1121            'error' => $message,
1122            'detail' => $detail,
1123            'smtp_code' => $smtp_code,
1124            'smtp_code_ex' => $smtp_code_ex
1125        );
1126    }
1127
1128    /**
1129     * Set debug output method.
1130     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it.
1131     */
1132    public function setDebugOutput($method = 'echo')
1133    {
1134        $this->Debugoutput = $method;
1135    }
1136
1137    /**
1138     * Get debug output method.
1139     * @return string
1140     */
1141    public function getDebugOutput()
1142    {
1143        return $this->Debugoutput;
1144    }
1145
1146    /**
1147     * Set debug output level.
1148     * @param integer $level
1149     */
1150    public function setDebugLevel($level = 0)
1151    {
1152        $this->do_debug = $level;
1153    }
1154
1155    /**
1156     * Get debug output level.
1157     * @return integer
1158     */
1159    public function getDebugLevel()
1160    {
1161        return $this->do_debug;
1162    }
1163
1164    /**
1165     * Set SMTP timeout.
1166     * @param integer $timeout
1167     */
1168    public function setTimeout($timeout = 0)
1169    {
1170        $this->Timeout = $timeout;
1171    }
1172
1173    /**
1174     * Get SMTP timeout.
1175     * @return integer
1176     */
1177    public function getTimeout()
1178    {
1179        return $this->Timeout;
1180    }
1181}
Note: See TracBrowser for help on using the repository browser.