SMTP::authenticate()   F
last analyzed

Complexity

Conditions 26
Paths 107

Size

Total Lines 140

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
nc 107
nop 6
dl 0
loc 140
rs 3.2866
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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) <[email protected]>
8
 * @author    Jim Jagielski (jimjag) <[email protected]>
9
 * @author    Andy Prevost (codeworxtech) <[email protected]>
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 <[email protected]>
26
 */
27
class SMTP
28
{
29
    /**
30
     * The PHPMailer SMTP version number.
31
     * @var string
32
     */
33
    const VERSION = '5.2.26';
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 int
44
     */
45
    const DEFAULT_SMTP_PORT = 25;
46
47
    /**
48
     * The maximum line length allowed by RFC 2822 section 2.1.1
49
     * @var int
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.26';
85
86
    /**
87
     * SMTP server port number.
88
     * @var int
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 int
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 bool
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 int
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 int
150
     */
151
    public $Timelimit = 300;
152
153
    /**
154
     * @var array Patterns to extract an SMTP transaction id from reply to a DATA command.
155
     * The first capture group in each regex will be used as the ID.
156
     */
157
    protected $smtp_transaction_id_patterns = [
158
        'exim'     => '/[0-9]{3} OK id=(.*)/',
159
        'sendmail' => '/[0-9]{3} 2.0.0 (.*) Message/',
160
        'postfix'  => '/[0-9]{3} 2.0.0 Ok: queued as (.*)/',
161
    ];
162
163
    /**
164
     * @var string The last transaction ID issued in response to a DATA command,
165
     * if one was detected
166
     */
167
    protected $last_smtp_transaction_id;
168
169
    /**
170
     * The socket for the server connection.
171
     * @var resource
172
     */
173
    protected $smtp_conn;
174
175
    /**
176
     * Error information, if any, for the last SMTP command.
177
     * @var array
178
     */
179
    protected $error = [
180
        'error'        => '',
181
        'detail'       => '',
182
        'smtp_code'    => '',
183
        'smtp_code_ex' => '',
184
    ];
185
186
    /**
187
     * The reply the server sent to us for HELO.
188
     * If null, no HELO string has yet been received.
189
     * @var string|null
190
     */
191
    protected $helo_rply = null;
192
193
    /**
194
     * The set of SMTP extensions sent in reply to EHLO command.
195
     * Indexes of the array are extension names.
196
     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
197
     * represents the server name. In case of HELO it is the only element of the array.
198
     * Other values can be boolean TRUE or an array containing extension options.
199
     * If null, no HELO/EHLO string has yet been received.
200
     * @var array|null
201
     */
202
    protected $server_caps = null;
203
204
    /**
205
     * The most recent reply received from the server.
206
     * @var string
207
     */
208
    protected $last_reply = '';
209
210
    /**
211
     * Output debugging info via a user-selected method.
212
     * @see SMTP::$Debugoutput
213
     * @see SMTP::$do_debug
214
     * @param string $str   Debug string to output
215
     * @param int    $level The debug level of this message; see DEBUG_* constants
216
     */
217
    protected function edebug($str, $level = 0)
218
    {
219
        if ($level > $this->do_debug) {
220
            return;
221
        }
222
        //Avoid clash with built-in function names
223 View Code Duplication
        if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo'], true) and is_callable($this->Debugoutput)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
224
            call_user_func($this->Debugoutput, $str, $level);
225
226
            return;
227
        }
228
        switch ($this->Debugoutput) {
229
            case 'error_log':
230
                //Don't output, just log
231
                error_log($str);
232
                break;
233
            case 'html':
234
                //Cleans up output a bit for a better looking, HTML-safe output
235
                echo gmdate('Y-m-d H:i:s') . ' ' . htmlentities(preg_replace('/[\r\n]+/', '', $str), ENT_QUOTES, 'UTF-8') . "<br>\n";
236
                break;
237
            case 'echo':
238 View Code Duplication
            default:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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("\n", "\n                   \t                  ", trim($str)) . "\n";
242
        }
243
    }
244
245
    /**
246
     * Connect to an SMTP server.
247
     * @param string $host    SMTP server IP or host name
248
     * @param int    $port    The port number to connect to
0 ignored issues
show
Documentation introduced by
Should the type for parameter $port not be integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
249
     * @param int    $timeout How long to wait for the connection to open
250
     * @param array  $options An array of options for stream_context_create()
251
     * @access public
252
     * @return bool
253
     */
254
    public function connect($host, $port = null, $timeout = 30, $options = [])
255
    {
256
        static $streamok;
257
        //This is enabled by default since 5.0.0 but some providers disable it
258
        //Check this once and cache the result
259
        if (null === $streamok) {
260
            $streamok = function_exists('stream_socket_client');
261
        }
262
        // Clear errors to avoid confusion
263
        $this->setError('');
264
        // Make sure we are __not__ connected
265
        if ($this->connected()) {
266
            // Already connected, generate error
267
            $this->setError('Already connected to a server');
268
269
            return false;
270
        }
271
        if (empty($port)) {
272
            $port = self::DEFAULT_SMTP_PORT;
273
        }
274
        // Connect to the SMTP server
275
        $this->edebug("Connection: opening to $host:$port, timeout=$timeout, options=" . var_export($options, true), self::DEBUG_CONNECTION);
276
        $errno  = 0;
277
        $errstr = '';
278
        if ($streamok) {
279
            $socket_context = stream_context_create($options);
280
            set_error_handler([$this, 'errorHandler']);
281
            $this->smtp_conn = stream_socket_client($host . ':' . $port, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $socket_context);
282
            restore_error_handler();
283
        } else {
284
            //Fall back to fsockopen which should work in more places, but is missing some features
285
            $this->edebug('Connection: stream_socket_client not available, falling back to fsockopen', self::DEBUG_CONNECTION);
286
            set_error_handler([$this, 'errorHandler']);
287
            $this->smtp_conn = fsockopen($host, $port, $errno, $errstr, $timeout);
288
            restore_error_handler();
289
        }
290
        // Verify we connected properly
291
        if (!is_resource($this->smtp_conn)) {
292
            $this->setError('Failed to connect to server', $errno, $errstr);
293
            $this->edebug('SMTP ERROR: ' . $this->error['error'] . ": $errstr ($errno)", self::DEBUG_CLIENT);
294
295
            return false;
296
        }
297
        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
298
        // SMTP server can take longer to respond, give longer timeout for first read
299
        // Windows does not have support for this timeout function
300
        if ('WIN' != mb_substr(PHP_OS, 0, 3)) {
301
            $max = ini_get('max_execution_time');
302
            // Don't bother if unlimited
303
            if (0 != $max && $timeout > $max) {
304
                @set_time_limit($timeout);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
305
            }
306
            stream_set_timeout($this->smtp_conn, $timeout, 0);
307
        }
308
        // Get any announcement
309
        $announce = $this->get_lines();
310
        $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
311
312
        return true;
313
    }
314
315
    /**
316
     * Initiate a TLS (encrypted) session.
317
     * @access public
318
     * @return bool
319
     */
320
    public function startTLS()
321
    {
322
        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
323
            return false;
324
        }
325
326
        //Allow the best TLS version(s) we can
327
        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
328
329
        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
330
        //so add them back in manually if we can
331
        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
332
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
333
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
334
        }
335
336
        // Begin encrypted connection
337
        set_error_handler([$this, 'errorHandler']);
338
        $crypto_ok = stream_socket_enable_crypto($this->smtp_conn, true, $crypto_method);
339
        restore_error_handler();
340
341
        return $crypto_ok;
342
    }
343
344
    /**
345
     * Perform SMTP authentication.
346
     * Must be run after hello().
347
     * @see hello()
348
     * @param string     $username    The user name
349
     * @param string     $password    The password
350
     * @param string     $authtype    The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5, XOAUTH2)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $authtype not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
351
     * @param string     $realm       The auth realm for NTLM
352
     * @param string     $workstation The auth workstation for NTLM
353
     * @param null|OAuth $OAuth       An optional OAuth instance (@see PHPMailerOAuth)
354
     * @return bool True if successfully authenticated.* @access public
355
     */
356
    public function authenticate(
357
        $username,
358
        $password,
359
        $authtype = null,
360
        $realm = '',
361
        $workstation = '',
362
        $OAuth = null)
363
    {
364
        if (!$this->server_caps) {
365
            $this->setError('Authentication is not allowed before HELO/EHLO');
366
367
            return false;
368
        }
369
370
        if (array_key_exists('EHLO', $this->server_caps)) {
371
            // SMTP extensions are available; try to find a proper authentication method
372
            if (!array_key_exists('AUTH', $this->server_caps)) {
373
                $this->setError('Authentication is not allowed at this stage');
374
                // 'at this stage' means that auth may be allowed after the stage changes
375
                // e.g. after STARTTLS
376
                return false;
377
            }
378
379
            self::edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL);
380
            self::edebug('Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']), self::DEBUG_LOWLEVEL);
381
382
            if (empty($authtype)) {
383
                foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'NTLM', 'XOAUTH2'] as $method) {
384
                    if (in_array($method, $this->server_caps['AUTH'], true)) {
385
                        $authtype = $method;
386
                        break;
387
                    }
388
                }
389
                if (empty($authtype)) {
390
                    $this->setError('No supported authentication methods found');
391
392
                    return false;
393
                }
394
                self::edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
395
            }
396
397
            if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
398
                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
399
400
                return false;
401
            }
402
        } elseif (empty($authtype)) {
403
            $authtype = 'LOGIN';
404
        }
405
        switch ($authtype) {
406
            case 'PLAIN':
407
                // Start authentication
408
                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
409
                    return false;
410
                }
411
                // Send encoded username and password
412
                if (!$this->sendCommand('User & Password', base64_encode("\0" . $username . "\0" . $password), 235)) {
413
                    return false;
414
                }
415
                break;
416
            case 'LOGIN':
417
                // Start authentication
418
                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
419
                    return false;
420
                }
421
                if (!$this->sendCommand('Username', base64_encode($username), 334)) {
422
                    return false;
423
                }
424
                if (!$this->sendCommand('Password', base64_encode($password), 235)) {
425
                    return false;
426
                }
427
                break;
428
            case 'XOAUTH2':
429
                //If the OAuth Instance is not set. Can be a case when PHPMailer is used
430
                //instead of PHPMailerOAuth
431
                if (null === $OAuth) {
432
                    return false;
433
                }
434
                $oauth = $OAuth->getOauth64();
435
436
                // Start authentication
437
                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
438
                    return false;
439
                }
440
                break;
441
            case 'NTLM':
442
                /*
443
                 * ntlm_sasl_client.php
444
                 * Bundled with Permission
445
                 *
446
                 * How to telnet in windows:
447
                 * http://technet.microsoft.com/en-us/library/aa995718%28EXCHG.65%29.aspx
448
                 * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication
449
                 */
450
                require_once 'extras/ntlm_sasl_client.php';
451
                $temp        = new stdClass();
452
                $ntlm_client = new ntlm_sasl_client_class();
453
                //Check that functions are available
454
                if (!$ntlm_client->initialize($temp)) {
455
                    $this->setError($temp->error);
456
                    $this->edebug('You need to enable some modules in your php.ini file: ' . $this->error['error'], self::DEBUG_CLIENT);
457
458
                    return false;
459
                }
460
                //msg1
461
                $msg1 = $ntlm_client->typeMsg1($realm, $workstation); //msg1
462
463
                if (!$this->sendCommand('AUTH NTLM', 'AUTH NTLM ' . base64_encode($msg1), 334)) {
464
                    return false;
465
                }
466
                //Though 0 based, there is a white space after the 3 digit number
467
                //msg2
468
                $challenge = mb_substr($this->last_reply, 3);
469
                $challenge = base64_decode($challenge, true);
470
                $ntlm_res  = $ntlm_client->NTLMResponse(mb_substr($challenge, 24, 8), $password);
471
                //msg3
472
                $msg3 = $ntlm_client->typeMsg3($ntlm_res, $username, $realm, $workstation);
473
                // send encoded username
474
                return $this->sendCommand('Username', base64_encode($msg3), 235);
475
            case 'CRAM-MD5':
476
                // Start authentication
477
                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
478
                    return false;
479
                }
480
                // Get the challenge
481
                $challenge = base64_decode(mb_substr($this->last_reply, 4), true);
482
483
                // Build the response
484
                $response = $username . ' ' . $this->hmac($challenge, $password);
485
486
                // send encoded credentials
487
                return $this->sendCommand('Username', base64_encode($response), 235);
488
            default:
489
                $this->setError("Authentication method \"$authtype\" is not supported");
490
491
                return false;
492
        }
493
494
        return true;
495
    }
496
497
    /**
498
     * Calculate an MD5 HMAC hash.
499
     * Works like hash_hmac('md5', $data, $key)
500
     * in case that function is not available
501
     * @param string $data The data to hash
502
     * @param string $key  The key to hash with
503
     * @access protected
504
     * @return string
505
     */
506
    protected function hmac($data, $key)
507
    {
508
        if (function_exists('hash_hmac')) {
509
            return hash_hmac('md5', $data, $key);
510
        }
511
512
        // The following borrowed from
513
        // http://php.net/manual/en/function.mhash.php#27225
514
515
        // RFC 2104 HMAC implementation for php.
516
        // Creates an md5 HMAC.
517
        // Eliminates the need to install mhash to compute a HMAC
518
        // by Lance Rushing
519
520
        $bytelen = 64; // byte length for md5
521
        if (mb_strlen($key) > $bytelen) {
522
            $key = pack('H*', md5($key));
523
        }
524
        $key    = str_pad($key, $bytelen, chr(0x00));
525
        $ipad   = str_pad('', $bytelen, chr(0x36));
526
        $opad   = str_pad('', $bytelen, chr(0x5c));
527
        $k_ipad = $key ^ $ipad;
528
        $k_opad = $key ^ $opad;
529
530
        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
531
    }
532
533
    /**
534
     * Check connection state.
535
     * @access public
536
     * @return bool True if connected.
537
     */
538
    public function connected()
539
    {
540
        if (is_resource($this->smtp_conn)) {
541
            $sock_status = stream_get_meta_data($this->smtp_conn);
542
            if ($sock_status['eof']) {
543
                // The socket is valid but we are not connected
544
                $this->edebug('SMTP NOTICE: EOF caught while checking if connected', self::DEBUG_CLIENT);
545
                $this->close();
546
547
                return false;
548
            }
549
550
            return true; // everything looks good
551
        }
552
553
        return false;
554
    }
555
556
    /**
557
     * Close the socket and clean up the state of the class.
558
     * Don't use this function without first trying to use QUIT.
559
     * @see    quit()
560
     * @access public
561
     */
562
    public function close()
563
    {
564
        $this->setError('');
565
        $this->server_caps = null;
566
        $this->helo_rply   = null;
567
        if (is_resource($this->smtp_conn)) {
568
            // close the connection and cleanup
569
            fclose($this->smtp_conn);
570
            $this->smtp_conn = null; //Makes for cleaner serialization
571
            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
572
        }
573
    }
574
575
    /**
576
     * Send an SMTP DATA command.
577
     * Issues a data command and sends the msg_data to the server,
578
     * finializing the mail transaction. $msg_data is the message
579
     * that is to be send with the headers. Each header needs to be
580
     * on a single line followed by a <CRLF> with the message headers
581
     * and the message body being separated by and additional <CRLF>.
582
     * Implements rfc 821: DATA <CRLF>
583
     * @param string $msg_data Message data to send
584
     * @access public
585
     * @return bool
586
     */
587
    public function data($msg_data)
588
    {
589
        //This will use the standard timelimit
590
        if (!$this->sendCommand('DATA', 'DATA', 354)) {
591
            return false;
592
        }
593
594
        /* The server is ready to accept data!
595
         * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF)
596
         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
597
         * smaller lines to fit within the limit.
598
         * We will also look for lines that start with a '.' and prepend an additional '.'.
599
         * NOTE: this does not count towards line-length limit.
600
         */
601
602
        // Normalize line breaks before exploding
603
        $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
604
605
        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
606
         * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
607
         * process all lines before a blank line as headers.
608
         */
609
610
        $field      = mb_substr($lines[0], 0, mb_strpos($lines[0], ':'));
611
        $in_headers = false;
612
        if (!empty($field) && false === mb_strpos($field, ' ')) {
613
            $in_headers = true;
614
        }
615
616
        foreach ($lines as $line) {
617
            $lines_out = [];
618
            if ($in_headers and '' == $line) {
619
                $in_headers = false;
620
            }
621
            //Break this line up into several smaller lines if it's too long
622
            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
623
            while (isset($line[self::MAX_LINE_LENGTH])) {
624
                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
625
                //so as to avoid breaking in the middle of a word
626
                $pos = mb_strrpos(mb_substr($line, 0, self::MAX_LINE_LENGTH), ' ');
627
                //Deliberately matches both false and 0
628
                if (!$pos) {
629
                    //No nice break found, add a hard break
630
                    $pos         = self::MAX_LINE_LENGTH - 1;
631
                    $lines_out[] = mb_substr($line, 0, $pos);
632
                    $line        = mb_substr($line, $pos);
633
                } else {
634
                    //Break at the found point
635
                    $lines_out[] = mb_substr($line, 0, $pos);
636
                    //Move along by the amount we dealt with
637
                    $line = mb_substr($line, $pos + 1);
638
                }
639
                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
640
                if ($in_headers) {
641
                    $line = "\t" . $line;
642
                }
643
            }
644
            $lines_out[] = $line;
645
646
            //Send the lines to the server
647
            foreach ($lines_out as $line_out) {
648
                //RFC2821 section 4.5.2
649
                if (!empty($line_out) and '.' == $line_out[0]) {
650
                    $line_out = '.' . $line_out;
651
                }
652
                $this->client_send($line_out . self::CRLF);
653
            }
654
        }
655
656
        //Message data has been sent, complete the command
657
        //Increase timelimit for end of DATA command
658
        $savetimelimit   = $this->Timelimit;
659
        $this->Timelimit = $this->Timelimit * 2;
660
        $result          = $this->sendCommand('DATA END', '.', 250);
661
        $this->recordLastTransactionID();
662
        //Restore timelimit
663
        $this->Timelimit = $savetimelimit;
664
665
        return $result;
666
    }
667
668
    /**
669
     * Send an SMTP HELO or EHLO command.
670
     * Used to identify the sending server to the receiving server.
671
     * This makes sure that client and server are in a known state.
672
     * Implements RFC 821: HELO <SP> <domain> <CRLF>
673
     * and RFC 2821 EHLO.
674
     * @param string $host The host name or IP to connect to
675
     * @access public
676
     * @return bool
677
     */
678
    public function hello($host = '')
679
    {
680
        //Try extended hello first (RFC 2821)
681
        return (bool)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host));
682
    }
683
684
    /**
685
     * Send an SMTP HELO or EHLO command.
686
     * Low-level implementation used by hello()
687
     * @see    hello()
688
     * @param string $hello The HELO string
689
     * @param string $host  The hostname to say we are
690
     * @access protected
691
     * @return bool
692
     */
693
    protected function sendHello($hello, $host)
694
    {
695
        $noerror         = $this->sendCommand($hello, $hello . ' ' . $host, 250);
696
        $this->helo_rply = $this->last_reply;
697
        if ($noerror) {
698
            $this->parseHelloFields($hello);
699
        } else {
700
            $this->server_caps = null;
701
        }
702
703
        return $noerror;
704
    }
705
706
    /**
707
     * Parse a reply to HELO/EHLO command to discover server extensions.
708
     * In case of HELO, the only parameter that can be discovered is a server name.
709
     * @access protected
710
     * @param string $type - 'HELO' or 'EHLO'
711
     */
712
    protected function parseHelloFields($type)
713
    {
714
        $this->server_caps = [];
715
        $lines             = explode("\n", $this->helo_rply);
716
717
        foreach ($lines as $n => $s) {
718
            //First 4 chars contain response code followed by - or space
719
            $s = trim(mb_substr($s, 4));
720
            if (empty($s)) {
721
                continue;
722
            }
723
            $fields = explode(' ', $s);
724
            if (!empty($fields)) {
725
                if (!$n) {
726
                    $name   = $type;
727
                    $fields = $fields[0];
728
                } else {
729
                    $name = array_shift($fields);
730
                    switch ($name) {
731
                        case 'SIZE':
732
                            $fields = ($fields ? $fields[0] : 0);
733
                            break;
734
                        case 'AUTH':
735
                            if (!is_array($fields)) {
736
                                $fields = [];
737
                            }
738
                            break;
739
                        default:
740
                            $fields = true;
741
                    }
742
                }
743
                $this->server_caps[$name] = $fields;
744
            }
745
        }
746
    }
747
748
    /**
749
     * Send an SMTP MAIL command.
750
     * Starts a mail transaction from the email address specified in
751
     * $from. Returns true if successful or false otherwise. If True
752
     * the mail transaction is started and then one or more recipient
753
     * commands may be called followed by a data command.
754
     * Implements rfc 821: MAIL <SP> FROM:<reverse-path> <CRLF>
755
     * @param string $from Source address of this message
756
     * @access public
757
     * @return bool
758
     */
759
    public function mail($from)
760
    {
761
        $useVerp = ($this->do_verp ? ' XVERP' : '');
762
763
        return $this->sendCommand('MAIL FROM', 'MAIL FROM:<' . $from . '>' . $useVerp, 250);
764
    }
765
766
    /**
767
     * Send an SMTP QUIT command.
768
     * Closes the socket if there is no error or the $close_on_error argument is true.
769
     * Implements from rfc 821: QUIT <CRLF>
770
     * @param bool $close_on_error Should the connection close if an error occurs?
771
     * @access public
772
     * @return bool
773
     */
774
    public function quit($close_on_error = true)
775
    {
776
        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
777
        $err     = $this->error; //Save any error
778
        if ($noerror or $close_on_error) {
779
            $this->close();
780
            $this->error = $err; //Restore any error from the quit command
781
        }
782
783
        return $noerror;
784
    }
785
786
    /**
787
     * Send an SMTP RCPT command.
788
     * Sets the TO argument to $toaddr.
789
     * Returns true if the recipient was accepted false if it was rejected.
790
     * Implements from rfc 821: RCPT <SP> TO:<forward-path> <CRLF>
791
     * @param string $address The address the message is being sent to
792
     * @access public
793
     * @return bool
794
     */
795
    public function recipient($address)
796
    {
797
        return $this->sendCommand('RCPT TO', 'RCPT TO:<' . $address . '>', [250, 251]);
798
    }
799
800
    /**
801
     * Send an SMTP RSET command.
802
     * Abort any transaction that is currently in progress.
803
     * Implements rfc 821: RSET <CRLF>
804
     * @access public
805
     * @return bool True on success.
806
     */
807
    public function reset()
808
    {
809
        return $this->sendCommand('RSET', 'RSET', 250);
810
    }
811
812
    /**
813
     * Send a command to an SMTP server and check its return code.
814
     * @param string    $command       The command name - not sent to the server
815
     * @param string    $commandstring The actual command to send
816
     * @param int|array $expect        One or more expected integer success codes
817
     * @access protected
818
     * @return bool True on success.
819
     */
820
    protected function sendCommand($command, $commandstring, $expect)
821
    {
822
        if (!$this->connected()) {
823
            $this->setError("Called $command without being connected");
824
825
            return false;
826
        }
827
        //Reject line breaks in all commands
828
        if (false !== mb_strpos($commandstring, "\n") or false !== mb_strpos($commandstring, "\r")) {
829
            $this->setError("Command '$command' contained line breaks");
830
831
            return false;
832
        }
833
        $this->client_send($commandstring . self::CRLF);
834
835
        $this->last_reply = $this->get_lines();
836
        // Fetch SMTP code and possible error code explanation
837
        $matches = [];
838
        if (preg_match('/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/', $this->last_reply, $matches)) {
839
            $code    = $matches[1];
840
            $code_ex = (count($matches) > 2 ? $matches[2] : null);
841
            // Cut off error code from each response line
842
            $detail = preg_replace("/{$code}[ -]" . ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m', '', $this->last_reply);
843
        } else {
844
            // Fall back to simple parsing if regex fails
845
            $code    = mb_substr($this->last_reply, 0, 3);
846
            $code_ex = null;
847
            $detail  = mb_substr($this->last_reply, 4);
848
        }
849
850
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
851
852
        if (!in_array($code, (array)$expect, true)) {
853
            $this->setError("$command command failed", $detail, $code, $code_ex);
854
            $this->edebug('SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply, self::DEBUG_CLIENT);
855
856
            return false;
857
        }
858
859
        $this->setError('');
860
861
        return true;
862
    }
863
864
    /**
865
     * Send an SMTP SAML command.
866
     * Starts a mail transaction from the email address specified in $from.
867
     * Returns true if successful or false otherwise. If True
868
     * the mail transaction is started and then one or more recipient
869
     * commands may be called followed by a data command. This command
870
     * will send the message to the users terminal if they are logged
871
     * in and send them an email.
872
     * Implements rfc 821: SAML <SP> FROM:<reverse-path> <CRLF>
873
     * @param string $from The address the message is from
874
     * @access public
875
     * @return bool
876
     */
877
    public function sendAndMail($from)
878
    {
879
        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
880
    }
881
882
    /**
883
     * Send an SMTP VRFY command.
884
     * @param string $name The name to verify
885
     * @access public
886
     * @return bool
887
     */
888
    public function verify($name)
889
    {
890
        return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
891
    }
892
893
    /**
894
     * Send an SMTP NOOP command.
895
     * Used to keep keep-alives alive, doesn't actually do anything
896
     * @access public
897
     * @return bool
898
     */
899
    public function noop()
900
    {
901
        return $this->sendCommand('NOOP', 'NOOP', 250);
902
    }
903
904
    /**
905
     * Send an SMTP TURN command.
906
     * This is an optional command for SMTP that this class does not support.
907
     * This method is here to make the RFC821 Definition complete for this class
908
     * and _may_ be implemented in future
909
     * Implements from rfc 821: TURN <CRLF>
910
     * @access public
911
     * @return bool
912
     */
913
    public function turn()
914
    {
915
        $this->setError('The SMTP TURN command is not implemented');
916
        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
917
918
        return false;
919
    }
920
921
    /**
922
     * Send raw data to the server.
923
     * @param string $data The data to send
924
     * @access public
925
     * @return int|bool The number of bytes sent to the server or false on error
926
     */
927
    public function client_send($data)
928
    {
929
        $this->edebug("CLIENT -> SERVER: $data", self::DEBUG_CLIENT);
930
        set_error_handler([$this, 'errorHandler']);
931
        $result = fwrite($this->smtp_conn, $data);
932
        restore_error_handler();
933
934
        return $result;
935
    }
936
937
    /**
938
     * Get the latest error.
939
     * @access public
940
     * @return array
941
     */
942
    public function getError()
943
    {
944
        return $this->error;
945
    }
946
947
    /**
948
     * Get SMTP extensions available on the server
949
     * @access public
950
     * @return array|null
951
     */
952
    public function getServerExtList()
953
    {
954
        return $this->server_caps;
955
    }
956
957
    /**
958
     * A multipurpose method
959
     * The method works in three ways, dependent on argument value and current state
960
     *   1. HELO/EHLO was not sent - returns null and set up $this->error
961
     *   2. HELO was sent
962
     *     $name = 'HELO': returns server name
963
     *     $name = 'EHLO': returns boolean false
964
     *     $name = any string: returns null and set up $this->error
965
     *   3. EHLO was sent
966
     *     $name = 'HELO'|'EHLO': returns server name
967
     *     $name = any string: if extension $name exists, returns boolean True
968
     *       or its options. Otherwise returns boolean False
969
     * In other words, one can use this method to detect 3 conditions:
970
     *  - null returned: handshake was not or we don't know about ext (refer to $this->error)
971
     *  - false returned: the requested feature exactly not exists
972
     *  - positive value returned: the requested feature exists
973
     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
974
     * @return mixed
975
     */
976
    public function getServerExt($name)
977
    {
978
        if (!$this->server_caps) {
979
            $this->setError('No HELO/EHLO was sent');
980
981
            return null;
982
        }
983
984
        // the tight logic knot ;)
985
        if (!array_key_exists($name, $this->server_caps)) {
986
            if ('HELO' == $name) {
987
                return $this->server_caps['EHLO'];
988
            }
989
            if ('EHLO' == $name || array_key_exists('EHLO', $this->server_caps)) {
990
                return false;
991
            }
992
            $this->setError('HELO handshake was used. Client knows nothing about server extensions');
993
994
            return null;
995
        }
996
997
        return $this->server_caps[$name];
998
    }
999
1000
    /**
1001
     * Get the last reply from the server.
1002
     * @access public
1003
     * @return string
1004
     */
1005
    public function getLastReply()
1006
    {
1007
        return $this->last_reply;
1008
    }
1009
1010
    /**
1011
     * Read the SMTP server's response.
1012
     * Either before eof or socket timeout occurs on the operation.
1013
     * With SMTP we can tell if we have more lines to read if the
1014
     * 4th character is '-' symbol. If it is a space then we don't
1015
     * need to read anything else.
1016
     * @access protected
1017
     * @return string
1018
     */
1019
    protected function get_lines()
1020
    {
1021
        // If the connection is bad, give up straight away
1022
        if (!is_resource($this->smtp_conn)) {
1023
            return '';
1024
        }
1025
        $data    = '';
1026
        $endtime = 0;
1027
        stream_set_timeout($this->smtp_conn, $this->Timeout);
1028
        if ($this->Timelimit > 0) {
1029
            $endtime = time() + $this->Timelimit;
1030
        }
1031
        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1032
            $str = @fgets($this->smtp_conn, 515);
1033
            $this->edebug("SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL);
1034
            $this->edebug("SMTP -> get_lines(): \$str is  \"$str\"", self::DEBUG_LOWLEVEL);
1035
            $data .= $str;
1036
            // If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
1037
            // or 4th character is a space, we are done reading, break the loop,
1038
            // string array access is a micro-optimisation over strlen
1039
            if (!isset($str[3]) or (isset($str[3]) and ' ' == $str[3])) {
1040
                break;
1041
            }
1042
            // Timed-out? Log and break
1043
            $info = stream_get_meta_data($this->smtp_conn);
1044
            if ($info['timed_out']) {
1045
                $this->edebug('SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)', self::DEBUG_LOWLEVEL);
1046
                break;
1047
            }
1048
            // Now check if reads took too long
1049
            if ($endtime and time() > $endtime) {
1050
                $this->edebug('SMTP -> get_lines(): timelimit reached (' . $this->Timelimit . ' sec)', self::DEBUG_LOWLEVEL);
1051
                break;
1052
            }
1053
        }
1054
1055
        return $data;
1056
    }
1057
1058
    /**
1059
     * Enable or disable VERP address generation.
1060
     * @param bool $enabled
1061
     */
1062
    public function setVerp($enabled = false)
1063
    {
1064
        $this->do_verp = $enabled;
1065
    }
1066
1067
    /**
1068
     * Get VERP address generation mode.
1069
     * @return bool
1070
     */
1071
    public function getVerp()
1072
    {
1073
        return $this->do_verp;
1074
    }
1075
1076
    /**
1077
     * Set error messages and codes.
1078
     * @param string $message      The error message
1079
     * @param string $detail       Further detail on the error
1080
     * @param string $smtp_code    An associated SMTP error code
1081
     * @param string $smtp_code_ex Extended SMTP code
1082
     */
1083
    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1084
    {
1085
        $this->error = [
1086
            'error'        => $message,
1087
            'detail'       => $detail,
1088
            'smtp_code'    => $smtp_code,
1089
            'smtp_code_ex' => $smtp_code_ex,
1090
        ];
1091
    }
1092
1093
    /**
1094
     * Set debug output method.
1095
     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it.
1096
     */
1097
    public function setDebugOutput($method = 'echo')
1098
    {
1099
        $this->Debugoutput = $method;
1100
    }
1101
1102
    /**
1103
     * Get debug output method.
1104
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be callable?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1105
     */
1106
    public function getDebugOutput()
1107
    {
1108
        return $this->Debugoutput;
1109
    }
1110
1111
    /**
1112
     * Set debug output level.
1113
     * @param int $level
1114
     */
1115
    public function setDebugLevel($level = 0)
1116
    {
1117
        $this->do_debug = $level;
1118
    }
1119
1120
    /**
1121
     * Get debug output level.
1122
     * @return int
1123
     */
1124
    public function getDebugLevel()
1125
    {
1126
        return $this->do_debug;
1127
    }
1128
1129
    /**
1130
     * Set SMTP timeout.
1131
     * @param int $timeout
1132
     */
1133
    public function setTimeout($timeout = 0)
1134
    {
1135
        $this->Timeout = $timeout;
1136
    }
1137
1138
    /**
1139
     * Get SMTP timeout.
1140
     * @return int
1141
     */
1142
    public function getTimeout()
1143
    {
1144
        return $this->Timeout;
1145
    }
1146
1147
    /**
1148
     * Reports an error number and string.
1149
     * @param int    $errno   The error number returned by PHP.
1150
     * @param string $errmsg  The error message returned by PHP.
1151
     * @param string $errfile The file the error occurred in
1152
     * @param int    $errline The line number the error occurred on
1153
     */
1154
    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1155
    {
1156
        $notice = 'Connection failed.';
1157
        $this->setError($notice, $errno, $errmsg);
1158
        $this->edebug($notice . ' Error #' . $errno . ': ' . $errmsg . " [$errfile line $errline]", self::DEBUG_CONNECTION);
1159
    }
1160
1161
    /**
1162
     * Extract and return the ID of the last SMTP transaction based on
1163
     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1164
     * Relies on the host providing the ID in response to a DATA command.
1165
     * If no reply has been received yet, it will return null.
1166
     * If no pattern was matched, it will return false.
1167
     * @return bool|null|string
1168
     */
1169
    protected function recordLastTransactionID()
1170
    {
1171
        $reply = $this->getLastReply();
1172
1173
        if (empty($reply)) {
1174
            $this->last_smtp_transaction_id = null;
1175
        } else {
1176
            $this->last_smtp_transaction_id = false;
0 ignored issues
show
Documentation Bug introduced by
The property $last_smtp_transaction_id was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1177
            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1178
                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1179
                    $this->last_smtp_transaction_id = $matches[1];
1180
                }
1181
            }
1182
        }
1183
1184
        return $this->last_smtp_transaction_id;
1185
    }
1186
1187
    /**
1188
     * Get the queue/transaction ID of the last SMTP transaction
1189
     * If no reply has been received yet, it will return null.
1190
     * If no pattern was matched, it will return false.
1191
     * @return bool|null|string
1192
     * @see recordLastTransactionID()
1193
     */
1194
    public function getLastTransactionID()
1195
    {
1196
        return $this->last_smtp_transaction_id;
1197
    }
1198
}
1199