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

Last change on this file since 101345 was 101345, checked in by brunobergot@…, 3 years ago

version 3.4.6 : sécurité, maj de la lib PHPMailer en version 5.2.19

on en profite pour ajouter PHPMailer aux crédits du plugin

astuce en cadeau, les commandes svn pour retirer les fichiers absents ou ajouteur les nouveaux :

svn rm $( svn status | sed -e '/^!/!d' -e 's/^!//' ) + svn add $( svn status | sed -e '/^?/!d' -e 's/^?//' )

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