Passed
Push — master ( bbacda...9d4008 )
by Marcus
43:43
created

src/SMTP.php (12 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * PHPMailer RFC821 SMTP email transport class.
4
 * PHP Version 5.5
5
 *
6
 * @package   PHPMailer
7
 * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
8
 * @author    Marcus Bointon (Synchro/coolbru) <[email protected]>
9
 * @author    Jim Jagielski (jimjag) <[email protected]>
10
 * @author    Andy Prevost (codeworxtech) <[email protected]>
11
 * @author    Brent R. Matzelle (original founder)
12
 * @copyright 2012 - 2016 Marcus Bointon
13
 * @copyright 2010 - 2012 Jim Jagielski
14
 * @copyright 2004 - 2009 Andy Prevost
15
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
16
 * @note      This program is distributed in the hope that it will be useful - WITHOUT
17
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18
 * FITNESS FOR A PARTICULAR PURPOSE.
19
 */
20
21
namespace PHPMailer\PHPMailer;
22
23
/**
24
 * PHPMailer RFC821 SMTP email transport class.
25
 * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
26
 *
27
 * @package PHPMailer
28
 * @author  Chris Ryan
29
 * @author  Marcus Bointon <[email protected]>
30
 */
31
class SMTP
0 ignored issues
show
This class has a complexity of 135 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

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

Loading history...
This class has 1248 lines of code which exceeds the configured maximum of 1000.

Really long classes often contain too much logic and violate the single responsibility principle.

We suggest to take a look at the “Code” section for options on how to refactor this code.

Loading history...
32
{
33
    /**
34
     * The PHPMailer SMTP version number.
35
     *
36
     * @var string
37
     */
38
    const VERSION = '6.0.0';
39
40
    /**
41
     * SMTP line break constant.
42
     *
43
     * @var string
44
     */
45
    const LE = "\r\n";
46
47
    /**
48
     * The SMTP port to use if one is not specified.
49
     *
50
     * @var integer
51
     */
52
    const DEFAULT_PORT = 25;
53
54
    /**
55
     * The maximum line length allowed by RFC 2822 section 2.1.1
56
     *
57
     * @var integer
58
     */
59
    const MAX_LINE_LENGTH = 998;
60
61
    /**
62
     * Debug level for no output
63
     */
64
    const DEBUG_OFF = 0;
65
66
    /**
67
     * Debug level to show client -> server messages
68
     */
69
    const DEBUG_CLIENT = 1;
70
71
    /**
72
     * Debug level to show client -> server and server -> client messages
73
     */
74
    const DEBUG_SERVER = 2;
75
76
    /**
77
     * Debug level to show connection status, client -> server and server -> client messages
78
     */
79
    const DEBUG_CONNECTION = 3;
80
81
    /**
82
     * Debug level to show all messages
83
     */
84
    const DEBUG_LOWLEVEL = 4;
85
86
    /**
87
     * Debug output level.
88
     * Options:
89
     * * self::DEBUG_OFF (`0`) No debug output, default
90
     * * self::DEBUG_CLIENT (`1`) Client commands
91
     * * self::DEBUG_SERVER (`2`) Client commands and server responses
92
     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
93
     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages
94
     *
95
     * @var integer
96
     */
97
    public $do_debug = self::DEBUG_OFF;
98
99
    /**
100
     * How to handle debug output.
101
     * Options:
102
     * * `echo` Output plain-text as-is, appropriate for CLI
103
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
104
     * * `error_log` Output to error log as configured in php.ini
105
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
106
     * <code>
107
     * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
108
     * </code>
109
     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
110
     * level output is used:
111
     * <code>
112
     * $mail->Debugoutput = new myPsr3Logger;
113
     * </code>
114
     *
115
     * @var string|callable|\Psr\Log\LoggerInterface
116
     */
117
    public $Debugoutput = 'echo';
118
119
    /**
120
     * Whether to use VERP.
121
     *
122
     * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
123
     * @see http://www.postfix.org/VERP_README.html Info on VERP
124
     * @var boolean
125
     */
126
    public $do_verp = false;
127
128
    /**
129
     * The timeout value for connection, in seconds.
130
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
131
     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
132
     *
133
     * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
134
     * @var integer
135
     */
136
    public $Timeout = 300;
137
138
    /**
139
     * How long to wait for commands to complete, in seconds.
140
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
141
     *
142
     * @var integer
143
     */
144
    public $Timelimit = 300;
145
146
    /**
147
     * @var array Patterns to extract an SMTP transaction id from reply to a DATA command.
148
     * The first capture group in each regex will be used as the ID.
149
     * MS ESMTP returns the message ID, which may not be correct for internal tracking.
150
     */
151
    protected $smtp_transaction_id_patterns = [
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $smtp_transaction_id_patterns exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
152
        'exim' => '/[0-9]{3} OK id=(.*)/',
153
        'sendmail' => '/[0-9]{3} 2.0.0 (.*) Message/',
154
        'postfix' => '/[0-9]{3} 2.0.0 Ok: queued as (.*)/',
155
        'Microsoft_ESMTP' => '/[0-9]{3} 2.[0-9].0 (.*)@(?:.*) Queued mail for delivery/'
156
    ];
157
158
    /**
159
     * @var string|boolean|null The last transaction ID issued in response to a DATA command,
160
     * if one was detected
161
     */
162
    protected $last_smtp_transaction_id;
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $last_smtp_transaction_id exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
163
164
    /**
165
     * The socket for the server connection.
166
     *
167
     * @var ?resource
168
     */
169
    protected $smtp_conn;
170
171
    /**
172
     * Error information, if any, for the last SMTP command.
173
     *
174
     * @var array
175
     */
176
    protected $error = [
177
        'error' => '',
178
        'detail' => '',
179
        'smtp_code' => '',
180
        'smtp_code_ex' => ''
181
    ];
182
183
    /**
184
     * The reply the server sent to us for HELO.
185
     * If null, no HELO string has yet been received.
186
     *
187
     * @var string|null
188
     */
189
    protected $helo_rply = null;
190
191
    /**
192
     * The set of SMTP extensions sent in reply to EHLO command.
193
     * Indexes of the array are extension names.
194
     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
195
     * represents the server name. In case of HELO it is the only element of the array.
196
     * Other values can be boolean TRUE or an array containing extension options.
197
     * If null, no HELO/EHLO string has yet been received.
198
     *
199
     * @var array|null
200
     */
201
    protected $server_caps = null;
202
203
    /**
204
     * The most recent reply received from the server.
205
     *
206
     * @var string
207
     */
208
    protected $last_reply = '';
209
210
    /**
211
     * Output debugging info via a user-selected method.
212
     *
213
     * @param string $str Debug string to output
214
     * @param integer $level The debug level of this message; see DEBUG_* constants
215
     *
216
     * @see SMTP::$Debugoutput
217
     * @see SMTP::$do_debug
218
     */
219
    protected function edebug($str, $level = 0)
220
    {
221
        if ($level > $this->do_debug) {
222
            return;
223
        }
224
        //Is this a PSR-3 logger?
225
        if (is_a($this->Debugoutput, 'Psr\Log\LoggerInterface')) {
226
            $this->Debugoutput->debug($str);
0 ignored issues
show
The method debug cannot be called on $this->Debugoutput (of type callable).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
227
            return;
228
        }
229
        //Avoid clash with built-in function names
230
        if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) {
231
            call_user_func($this->Debugoutput, $str, $level);
232
            return;
233
        }
234
        switch ($this->Debugoutput) {
235
            case 'error_log':
236
                //Don't output, just log
237
                error_log($str);
238
                break;
239
            case 'html':
240
                //Cleans up output a bit for a better looking, HTML-safe output
241
                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
242
                    preg_replace('/[\r\n]+/', '', $str),
243
                    ENT_QUOTES,
244
                    'UTF-8'
245
                ), "<br>\n";
246
                break;
247
            case 'echo':
248
            default:
249
                //Normalize line breaks
250
                $str = preg_replace('/\r\n|\r/ms', "\n", $str);
251
                echo gmdate('Y-m-d H:i:s'),
252
                    "\t",
253
                    //Trim trailing space
254
                    trim(
255
                        //Indent for readability, except for trailing break
256
                        str_replace(
257
                            "\n",
258
                            "\n                   \t                  ",
259
                            trim($str)
260
                        )
261
                    ),
262
                    "\n";
263
        }
264
    }
265
266
    /**
267
     * Connect to an SMTP server.
268
     *
269
     * @param string $host SMTP server IP or host name
270
     * @param integer $port The port number to connect to
271
     * @param integer $timeout How long to wait for the connection to open
272
     * @param array $options An array of options for stream_context_create()
273
     *
274
     * @return boolean
275
     */
276
    public function connect($host, $port = null, $timeout = 30, $options = [])
0 ignored issues
show
This operation has 640 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

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

Loading history...
277
    {
278
        static $streamok;
279
        //This is enabled by default since 5.0.0 but some providers disable it
280
        //Check this once and cache the result
281
        if (is_null($streamok)) {
282
            $streamok = function_exists('stream_socket_client');
283
        }
284
        // Clear errors to avoid confusion
285
        $this->setError('');
286
        // Make sure we are __not__ connected
287
        if ($this->connected()) {
288
            // Already connected, generate error
289
            $this->setError('Already connected to a server');
290
            return false;
291
        }
292
        if (empty($port)) {
293
            $port = self::DEFAULT_PORT;
294
        }
295
        // Connect to the SMTP server
296
        $this->edebug(
297
            "Connection: opening to $host:$port, timeout=$timeout, options=" .
298
            (count($options) > 0 ? var_export($options, true): 'array()'),
299
            self::DEBUG_CONNECTION
300
        );
301
        $errno = 0;
302
        $errstr = '';
303
        if ($streamok) {
304
            $socket_context = stream_context_create($options);
305
            set_error_handler([$this, 'errorHandler']);
306
            $this->smtp_conn = stream_socket_client(
307
                $host . ":" . $port,
308
                $errno,
309
                $errstr,
310
                $timeout,
311
                STREAM_CLIENT_CONNECT,
312
                $socket_context
313
            );
314
            restore_error_handler();
315
        } else {
316
            //Fall back to fsockopen which should work in more places, but is missing some features
317
            $this->edebug(
318
                "Connection: stream_socket_client not available, falling back to fsockopen",
319
                self::DEBUG_CONNECTION
320
            );
321
            set_error_handler([$this, 'errorHandler']);
322
            $this->smtp_conn = fsockopen(
323
                $host,
324
                $port,
325
                $errno,
326
                $errstr,
327
                $timeout
328
            );
329
            restore_error_handler();
330
        }
331
        // Verify we connected properly
332
        if (!is_resource($this->smtp_conn)) {
333
            $this->setError(
334
                'Failed to connect to server',
335
                '',
336
                (string)$errno,
337
                (string)$errstr
338
            );
339
            $this->edebug(
340
                'SMTP ERROR: ' . $this->error['error']
341
                . ": $errstr ($errno)",
342
                self::DEBUG_CLIENT
343
            );
344
            return false;
345
        }
346
        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
347
        // SMTP server can take longer to respond, give longer timeout for first read
348
        // Windows does not have support for this timeout function
349
        if (substr(PHP_OS, 0, 3) != 'WIN') {
350
            $max = ini_get('max_execution_time');
351
            // Don't bother if unlimited
352
            if (0 != $max and $timeout > $max) {
353
                @set_time_limit($timeout);
354
            }
355
            stream_set_timeout($this->smtp_conn, $timeout, 0);
356
        }
357
        // Get any announcement
358
        $announce = $this->get_lines();
359
        $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
360
        return true;
361
    }
362
363
    /**
364
     * Initiate a TLS (encrypted) session.
365
     *
366
     * @return boolean
367
     */
368
    public function startTLS()
369
    {
370
        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
371
            return false;
372
        }
373
374
        //Allow the best TLS version(s) we can
375
        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
376
377
        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
378
        //so add them back in manually if we can
379
        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
380
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
381
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
382
        }
383
384
        // Begin encrypted connection
385
        set_error_handler([$this, 'errorHandler']);
386
        $crypto_ok = stream_socket_enable_crypto(
387
            $this->smtp_conn,
388
            true,
389
            $crypto_method
390
        );
391
        restore_error_handler();
392
        return (boolean)$crypto_ok;
393
    }
394
395
    /**
396
     * Perform SMTP authentication.
397
     * Must be run after hello().
398
     *
399
     * @param string $username The user name
400
     * @param string $password The password
401
     * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
402
     * @param OAuth $OAuth An optional OAuth instance for XOAUTH2 authentication
403
     *
404
     * @return boolean True if successfully authenticated.
405
     * @see    hello()
406
     */
407
    public function authenticate(
0 ignored issues
show
This operation has 10716 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

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

Loading history...
408
        $username,
409
        $password,
410
        $authtype = null,
411
        $OAuth = null
412
    ) {
413
        if (!$this->server_caps) {
414
            $this->setError('Authentication is not allowed before HELO/EHLO');
415
            return false;
416
        }
417
418
        if (array_key_exists('EHLO', $this->server_caps)) {
419
            // SMTP extensions are available; try to find a proper authentication method
420
            if (!array_key_exists('AUTH', $this->server_caps)) {
421
                $this->setError('Authentication is not allowed at this stage');
422
                // 'at this stage' means that auth may be allowed after the stage changes
423
                // e.g. after STARTTLS
424
                return false;
425
            }
426
427
            $this->edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL);
428
            $this->edebug(
429
                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
430
                self::DEBUG_LOWLEVEL
431
            );
432
433
            //If we have requested a specific auth type, check the server supports it before trying others
434
            if (!in_array($authtype, $this->server_caps['AUTH'])) {
435
                $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
436
                $authtype = null;
437
            }
438
439
            if (empty($authtype)) {
440
                //If no auth mechanism is specified, attempt to use these, in this order
441
                //Try CRAM-MD5 first as it's more secure than the others
442
                foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
443
                    if (in_array($method, $this->server_caps['AUTH'])) {
444
                        $authtype = $method;
445
                        break;
446
                    }
447
                }
448
                if (empty($authtype)) {
449
                    $this->setError('No supported authentication methods found');
450
                    return false;
451
                }
452
                self::edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
453
            }
454
455
            if (!in_array($authtype, $this->server_caps['AUTH'])) {
456
                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
457
                return false;
458
            }
459
        } elseif (empty($authtype)) {
460
            $authtype = 'LOGIN';
461
        }
462
        switch ($authtype) {
463
            case 'PLAIN':
464
                // Start authentication
465
                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
466
                    return false;
467
                }
468
                // Send encoded username and password
469
                if (!$this->sendCommand(
470
                    'User & Password',
471
                    base64_encode("\0" . $username . "\0" . $password),
472
                    235
473
                )
474
                ) {
475
                    return false;
476
                }
477
                break;
478
            case 'LOGIN':
479
                // Start authentication
480
                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
481
                    return false;
482
                }
483
                if (!$this->sendCommand("Username", base64_encode($username), 334)) {
484
                    return false;
485
                }
486
                if (!$this->sendCommand("Password", base64_encode($password), 235)) {
487
                    return false;
488
                }
489
                break;
490
            case 'CRAM-MD5':
491
                // Start authentication
492
                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
493
                    return false;
494
                }
495
                // Get the challenge
496
                $challenge = base64_decode(substr($this->last_reply, 4));
497
498
                // Build the response
499
                $response = $username . ' ' . $this->hmac($challenge, $password);
500
501
                // send encoded credentials
502
                return $this->sendCommand('Username', base64_encode($response), 235);
503
            case 'XOAUTH2':
504
                //The OAuth instance must be set up prior to requesting auth.
505
                if (is_null($OAuth)) {
506
                    return false;
507
                }
508
                $oauth = $OAuth->getOauth64();
509
510
                // Start authentication
511
                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
512
                    return false;
513
                }
514
                break;
515
            default:
516
                $this->setError("Authentication method \"$authtype\" is not supported");
517
                return false;
518
        }
519
        return true;
520
    }
521
522
    /**
523
     * Calculate an MD5 HMAC hash.
524
     * Works like hash_hmac('md5', $data, $key)
525
     * in case that function is not available
526
     *
527
     * @param string $data The data to hash
528
     * @param string $key The key to hash with
529
     *
530
     * @return string
531
     */
532
    protected function hmac($data, $key)
533
    {
534
        if (function_exists('hash_hmac')) {
535
            return hash_hmac('md5', $data, $key);
536
        }
537
538
        // The following borrowed from
539
        // http://php.net/manual/en/function.mhash.php#27225
540
541
        // RFC 2104 HMAC implementation for php.
542
        // Creates an md5 HMAC.
543
        // Eliminates the need to install mhash to compute a HMAC
544
        // by Lance Rushing
545
546
        $bytelen = 64; // byte length for md5
547
        if (strlen($key) > $bytelen) {
548
            $key = pack('H*', md5($key));
549
        }
550
        $key = str_pad($key, $bytelen, chr(0x00));
551
        $ipad = str_pad('', $bytelen, chr(0x36));
552
        $opad = str_pad('', $bytelen, chr(0x5c));
553
        $k_ipad = $key ^ $ipad;
554
        $k_opad = $key ^ $opad;
555
556
        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
557
    }
558
559
    /**
560
     * Check connection state.
561
     *
562
     * @return boolean True if connected.
563
     */
564
    public function connected()
565
    {
566
        if (is_resource($this->smtp_conn)) {
567
            $sock_status = stream_get_meta_data($this->smtp_conn);
568
            if ($sock_status['eof']) {
569
                // The socket is valid but we are not connected
570
                $this->edebug(
571
                    'SMTP NOTICE: EOF caught while checking if connected',
572
                    self::DEBUG_CLIENT
573
                );
574
                $this->close();
575
                return false;
576
            }
577
            return true; // everything looks good
578
        }
579
        return false;
580
    }
581
582
    /**
583
     * Close the socket and clean up the state of the class.
584
     * Don't use this function without first trying to use QUIT.
585
     *
586
     * @see quit()
587
     */
588
    public function close()
589
    {
590
        $this->setError('');
591
        $this->server_caps = null;
592
        $this->helo_rply = null;
593
        if (is_resource($this->smtp_conn)) {
594
            // close the connection and cleanup
595
            fclose($this->smtp_conn);
596
            $this->smtp_conn = null; //Makes for cleaner serialization
597
            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
598
        }
599
    }
600
601
    /**
602
     * Send an SMTP DATA command.
603
     * Issues a data command and sends the msg_data to the server,
604
     * finializing the mail transaction. $msg_data is the message
605
     * that is to be send with the headers. Each header needs to be
606
     * on a single line followed by a <CRLF> with the message headers
607
     * and the message body being separated by an additional <CRLF>.
608
     * Implements RFC 821: DATA <CRLF>
609
     *
610
     * @param string $msg_data Message data to send
611
     *
612
     * @return boolean
613
     */
614
    public function data($msg_data)
0 ignored issues
show
This operation has 366 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

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

Loading history...
615
    {
616
        //This will use the standard timelimit
617
        if (!$this->sendCommand('DATA', 'DATA', 354)) {
618
            return false;
619
        }
620
621
        /* The server is ready to accept data!
622
         * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
623
         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
624
         * smaller lines to fit within the limit.
625
         * We will also look for lines that start with a '.' and prepend an additional '.'.
626
         * NOTE: this does not count towards line-length limit.
627
         */
628
629
        // Normalize line breaks before exploding
630
        $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
631
632
        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
633
         * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
634
         * process all lines before a blank line as headers.
635
         */
636
637
        $field = substr($lines[0], 0, strpos($lines[0], ':'));
638
        $in_headers = false;
639
        if (!empty($field) and strpos($field, ' ') === false) {
640
            $in_headers = true;
641
        }
642
643
        foreach ($lines as $line) {
644
            $lines_out = [];
645
            if ($in_headers and $line == '') {
646
                $in_headers = false;
647
            }
648
            //Break this line up into several smaller lines if it's too long
649
            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
650
            while (isset($line[self::MAX_LINE_LENGTH])) {
651
                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
652
                //so as to avoid breaking in the middle of a word
653
                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
654
                //Deliberately matches both false and 0
655
                if (!$pos) {
656
                    //No nice break found, add a hard break
657
                    $pos = self::MAX_LINE_LENGTH - 1;
658
                    $lines_out[] = substr($line, 0, $pos);
659
                    $line = substr($line, $pos);
660
                } else {
661
                    //Break at the found point
662
                    $lines_out[] = substr($line, 0, $pos);
663
                    //Move along by the amount we dealt with
664
                    $line = substr($line, $pos + 1);
665
                }
666
                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
667
                if ($in_headers) {
668
                    $line = "\t" . $line;
669
                }
670
            }
671
            $lines_out[] = $line;
672
673
            //Send the lines to the server
674
            foreach ($lines_out as $line_out) {
675
                //RFC2821 section 4.5.2
676
                if (!empty($line_out) and $line_out[0] == '.') {
677
                    $line_out = '.' . $line_out;
678
                }
679
                $this->client_send($line_out . static::LE);
680
            }
681
        }
682
683
        //Message data has been sent, complete the command
684
        //Increase timelimit for end of DATA command
685
        $savetimelimit = $this->Timelimit;
686
        $this->Timelimit = $this->Timelimit * 2;
687
        $result = $this->sendCommand('DATA END', '.', 250);
688
        $this->recordLastTransactionID();
689
        //Restore timelimit
690
        $this->Timelimit = $savetimelimit;
691
        return $result;
692
    }
693
694
    /**
695
     * Send an SMTP HELO or EHLO command.
696
     * Used to identify the sending server to the receiving server.
697
     * This makes sure that client and server are in a known state.
698
     * Implements RFC 821: HELO <SP> <domain> <CRLF>
699
     * and RFC 2821 EHLO.
700
     *
701
     * @param string $host The host name or IP to connect to
702
     *
703
     * @return boolean
704
     */
705
    public function hello($host = '')
706
    {
707
        //Try extended hello first (RFC 2821)
708
        return (boolean)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host));
709
    }
710
711
    /**
712
     * Send an SMTP HELO or EHLO command.
713
     * Low-level implementation used by hello()
714
     *
715
     * @param string $hello The HELO string
716
     * @param string $host The hostname to say we are
717
     *
718
     * @return boolean
719
     * @see    hello()
720
     */
721
    protected function sendHello($hello, $host)
722
    {
723
        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
724
        $this->helo_rply = $this->last_reply;
725
        if ($noerror) {
726
            $this->parseHelloFields($hello);
727
        } else {
728
            $this->server_caps = null;
729
        }
730
        return $noerror;
731
    }
732
733
    /**
734
     * Parse a reply to HELO/EHLO command to discover server extensions.
735
     * In case of HELO, the only parameter that can be discovered is a server name.
736
     *
737
     * @param string $type `HELO` or `EHLO`
738
     */
739
    protected function parseHelloFields($type)
740
    {
741
        $this->server_caps = [];
742
        $lines = explode("\n", $this->helo_rply);
743
744
        foreach ($lines as $n => $s) {
745
            //First 4 chars contain response code followed by - or space
746
            $s = trim(substr($s, 4));
747
            if (empty($s)) {
748
                continue;
749
            }
750
            $fields = explode(' ', $s);
751
            if (!empty($fields)) {
752
                if (!$n) {
753
                    $name = $type;
754
                    $fields = $fields[0];
755
                } else {
756
                    $name = array_shift($fields);
757
                    switch ($name) {
758
                        case 'SIZE':
759
                            $fields = ($fields ? $fields[0] : 0);
760
                            break;
761
                        case 'AUTH':
762
                            if (!is_array($fields)) {
763
                                $fields = [];
764
                            }
765
                            break;
766
                        default:
767
                            $fields = true;
768
                    }
769
                }
770
                $this->server_caps[$name] = $fields;
771
            }
772
        }
773
    }
774
775
    /**
776
     * Send an SMTP MAIL command.
777
     * Starts a mail transaction from the email address specified in
778
     * $from. Returns true if successful or false otherwise. If True
779
     * the mail transaction is started and then one or more recipient
780
     * commands may be called followed by a data command.
781
     * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>
782
     *
783
     * @param string $from Source address of this message
784
     *
785
     * @return boolean
786
     */
787
    public function mail($from)
788
    {
789
        $useVerp = ($this->do_verp ? ' XVERP' : '');
790
        return $this->sendCommand(
791
            'MAIL FROM',
792
            'MAIL FROM:<' . $from . '>' . $useVerp,
793
            250
794
        );
795
    }
796
797
    /**
798
     * Send an SMTP QUIT command.
799
     * Closes the socket if there is no error or the $close_on_error argument is true.
800
     * Implements from RFC 821: QUIT <CRLF>
801
     *
802
     * @param boolean $close_on_error Should the connection close if an error occurs?
803
     *
804
     * @return boolean
805
     */
806
    public function quit($close_on_error = true)
807
    {
808
        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
809
        $err = $this->error; //Save any error
810
        if ($noerror or $close_on_error) {
811
            $this->close();
812
            $this->error = $err; //Restore any error from the quit command
813
        }
814
        return $noerror;
815
    }
816
817
    /**
818
     * Send an SMTP RCPT command.
819
     * Sets the TO argument to $toaddr.
820
     * Returns true if the recipient was accepted false if it was rejected.
821
     * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>
822
     *
823
     * @param string $address The address the message is being sent to
824
     *
825
     * @return boolean
826
     */
827
    public function recipient($address)
828
    {
829
        return $this->sendCommand(
830
            'RCPT TO',
831
            'RCPT TO:<' . $address . '>',
832
            [250, 251]
833
        );
834
    }
835
836
    /**
837
     * Send an SMTP RSET command.
838
     * Abort any transaction that is currently in progress.
839
     * Implements RFC 821: RSET <CRLF>
840
     *
841
     * @return boolean True on success.
842
     */
843
    public function reset()
844
    {
845
        return $this->sendCommand('RSET', 'RSET', 250);
846
    }
847
848
    /**
849
     * Send a command to an SMTP server and check its return code.
850
     *
851
     * @param string $command The command name - not sent to the server
852
     * @param string $commandstring The actual command to send
853
     * @param integer|array $expect One or more expected integer success codes
854
     *
855
     * @return boolean True on success.
856
     */
857
    protected function sendCommand($command, $commandstring, $expect)
0 ignored issues
show
This operation has 312 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

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

Loading history...
858
    {
859
        if (!$this->connected()) {
860
            $this->setError("Called $command without being connected");
861
            return false;
862
        }
863
        //Reject line breaks in all commands
864
        if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) {
865
            $this->setError("Command '$command' contained line breaks");
866
            return false;
867
        }
868
        $this->client_send($commandstring . static::LE);
869
870
        $this->last_reply = $this->get_lines();
871
        // Fetch SMTP code and possible error code explanation
872
        $matches = [];
873
        if (preg_match("/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches)) {
874
            $code = $matches[1];
875
            $code_ex = (count($matches) > 2 ? $matches[2] : null);
876
            // Cut off error code from each response line
877
            $detail = preg_replace(
878
                "/{$code}[ -]" .
879
                ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . "/m",
880
                '',
881
                $this->last_reply
882
            );
883
        } else {
884
            // Fall back to simple parsing if regex fails
885
            $code = substr($this->last_reply, 0, 3);
886
            $code_ex = null;
887
            $detail = substr($this->last_reply, 4);
888
        }
889
890
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
891
892
        if (!in_array($code, (array)$expect)) {
893
            $this->setError(
894
                "$command command failed",
895
                $detail,
896
                $code,
897
                $code_ex
898
            );
899
            $this->edebug(
900
                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
901
                self::DEBUG_CLIENT
902
            );
903
            return false;
904
        }
905
906
        $this->setError('');
907
        return true;
908
    }
909
910
    /**
911
     * Send an SMTP SAML command.
912
     * Starts a mail transaction from the email address specified in $from.
913
     * Returns true if successful or false otherwise. If True
914
     * the mail transaction is started and then one or more recipient
915
     * commands may be called followed by a data command. This command
916
     * will send the message to the users terminal if they are logged
917
     * in and send them an email.
918
     * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>
919
     *
920
     * @param string $from The address the message is from
921
     *
922
     * @return boolean
923
     */
924
    public function sendAndMail($from)
925
    {
926
        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
927
    }
928
929
    /**
930
     * Send an SMTP VRFY command.
931
     *
932
     * @param string $name The name to verify
933
     *
934
     * @return boolean
935
     */
936
    public function verify($name)
937
    {
938
        return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
939
    }
940
941
    /**
942
     * Send an SMTP NOOP command.
943
     * Used to keep keep-alives alive, doesn't actually do anything
944
     *
945
     * @return boolean
946
     */
947
    public function noop()
948
    {
949
        return $this->sendCommand('NOOP', 'NOOP', 250);
950
    }
951
952
    /**
953
     * Send an SMTP TURN command.
954
     * This is an optional command for SMTP that this class does not support.
955
     * This method is here to make the RFC821 Definition complete for this class
956
     * and _may_ be implemented in future
957
     * Implements from RFC 821: TURN <CRLF>
958
     *
959
     * @return boolean
960
     */
961
    public function turn()
962
    {
963
        $this->setError('The SMTP TURN command is not implemented');
964
        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
965
        return false;
966
    }
967
968
    /**
969
     * Send raw data to the server.
970
     *
971
     * @param string $data The data to send
972
     *
973
     * @return integer|boolean The number of bytes sent to the server or false on error
974
     */
975
    public function client_send($data)
976
    {
977
        $this->edebug("CLIENT -> SERVER: $data", self::DEBUG_CLIENT);
978
        set_error_handler([$this, 'errorHandler']);
979
        $result = fwrite($this->smtp_conn, $data);
980
        restore_error_handler();
981
        return $result;
982
    }
983
984
    /**
985
     * Get the latest error.
986
     *
987
     * @return array
988
     */
989
    public function getError()
990
    {
991
        return $this->error;
992
    }
993
994
    /**
995
     * Get SMTP extensions available on the server
996
     *
997
     * @return array|null
998
     */
999
    public function getServerExtList()
1000
    {
1001
        return $this->server_caps;
1002
    }
1003
1004
    /**
1005
     * A multipurpose method
1006
     * The method works in three ways, dependent on argument value and current state
1007
     *   1. HELO/EHLO was not sent - returns null and set up $this->error
1008
     *   2. HELO was sent
1009
     *     $name = 'HELO': returns server name
1010
     *     $name = 'EHLO': returns boolean false
1011
     *     $name = any string: returns null and set up $this->error
1012
     *   3. EHLO was sent
1013
     *     $name = 'HELO'|'EHLO': returns server name
1014
     *     $name = any string: if extension $name exists, returns boolean True
1015
     *       or its options. Otherwise returns boolean False
1016
     * In other words, one can use this method to detect 3 conditions:
1017
     *  - null returned: handshake was not or we don't know about ext (refer to $this->error)
1018
     *  - false returned: the requested feature exactly not exists
1019
     *  - positive value returned: the requested feature exists
1020
     *
1021
     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
1022
     *
1023
     * @return mixed
1024
     */
1025
    public function getServerExt($name)
1026
    {
1027
        if (!$this->server_caps) {
1028
            $this->setError('No HELO/EHLO was sent');
1029
            return null;
1030
        }
1031
1032
        // the tight logic knot ;)
1033
        if (!array_key_exists($name, $this->server_caps)) {
1034
            if ('HELO' == $name) {
1035
                return $this->server_caps['EHLO'];
1036
            }
1037
            if ('EHLO' == $name || array_key_exists('EHLO', $this->server_caps)) {
1038
                return false;
1039
            }
1040
            $this->setError('HELO handshake was used. Client knows nothing about server extensions');
1041
            return null;
1042
        }
1043
1044
        return $this->server_caps[$name];
1045
    }
1046
1047
    /**
1048
     * Get the last reply from the server.
1049
     *
1050
     * @return string
1051
     */
1052
    public function getLastReply()
1053
    {
1054
        return $this->last_reply;
1055
    }
1056
1057
    /**
1058
     * Read the SMTP server's response.
1059
     * Either before eof or socket timeout occurs on the operation.
1060
     * With SMTP we can tell if we have more lines to read if the
1061
     * 4th character is '-' symbol. If it is a space then we don't
1062
     * need to read anything else.
1063
     *
1064
     * @return string
1065
     */
1066
    protected function get_lines()
0 ignored issues
show
This operation has 200 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

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

Loading history...
1067
    {
1068
        // If the connection is bad, give up straight away
1069
        if (!is_resource($this->smtp_conn)) {
1070
            return '';
1071
        }
1072
        $data = '';
1073
        $endtime = 0;
1074
        stream_set_timeout($this->smtp_conn, $this->Timeout);
1075
        if ($this->Timelimit > 0) {
1076
            $endtime = time() + $this->Timelimit;
1077
        }
1078
        $selR = [$this->smtp_conn];
1079
        $selW = null;
1080
        while (is_resource($this->smtp_conn) and !feof($this->smtp_conn)) {
1081
            //Must pass vars in here as params are by reference
1082
            if (!stream_select($selR, $selW, $selW, $this->Timelimit)) {
1083
                $this->edebug(
1084
                    'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)',
1085
                    self::DEBUG_LOWLEVEL
1086
                );
1087
                break;
1088
            }
1089
            //Deliberate noise suppression - errors are handled afterwards
1090
            $str = @fgets($this->smtp_conn, 515);
1091
            $this->edebug("SMTP INBOUND: \"". trim($str).'"', self::DEBUG_LOWLEVEL);
1092
            $data .= $str;
1093
            // If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
1094
            // or 4th character is a space, we are done reading, break the loop,
1095
            // string array access is a micro-optimisation over strlen
1096
            if (!isset($str[3]) or (isset($str[3]) and $str[3] == ' ')) {
1097
                break;
1098
            }
1099
            // Timed-out? Log and break
1100
            $info = stream_get_meta_data($this->smtp_conn);
1101
            if ($info['timed_out']) {
1102
                $this->edebug(
1103
                    'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)',
1104
                    self::DEBUG_LOWLEVEL
1105
                );
1106
                break;
1107
            }
1108
            // Now check if reads took too long
1109
            if ($endtime and time() > $endtime) {
1110
                $this->edebug(
1111
                    'SMTP -> get_lines(): timelimit reached (' .
1112
                    $this->Timelimit . ' sec)',
1113
                    self::DEBUG_LOWLEVEL
1114
                );
1115
                break;
1116
            }
1117
        }
1118
        return $data;
1119
    }
1120
1121
    /**
1122
     * Enable or disable VERP address generation.
1123
     *
1124
     * @param boolean $enabled
1125
     */
1126
    public function setVerp($enabled = false)
1127
    {
1128
        $this->do_verp = $enabled;
1129
    }
1130
1131
    /**
1132
     * Get VERP address generation mode.
1133
     *
1134
     * @return boolean
1135
     */
1136
    public function getVerp()
0 ignored issues
show
Coding Style Naming introduced by
The 'getVerp()' method which returns a boolean should be named 'is...()' or 'has...()'
Loading history...
1137
    {
1138
        return $this->do_verp;
1139
    }
1140
1141
    /**
1142
     * Set error messages and codes.
1143
     *
1144
     * @param string $message The error message
1145
     * @param string $detail Further detail on the error
1146
     * @param string $smtp_code An associated SMTP error code
1147
     * @param string $smtp_code_ex Extended SMTP code
1148
     */
1149
    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1150
    {
1151
        $this->error = [
1152
            'error' => $message,
1153
            'detail' => $detail,
1154
            'smtp_code' => $smtp_code,
1155
            'smtp_code_ex' => $smtp_code_ex
1156
        ];
1157
    }
1158
1159
    /**
1160
     * Set debug output method.
1161
     *
1162
     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it.
1163
     */
1164
    public function setDebugOutput($method = 'echo')
1165
    {
1166
        $this->Debugoutput = $method;
1167
    }
1168
1169
    /**
1170
     * Get debug output method.
1171
     *
1172
     * @return string
1173
     */
1174
    public function getDebugOutput()
1175
    {
1176
        return $this->Debugoutput;
1177
    }
1178
1179
    /**
1180
     * Set debug output level.
1181
     *
1182
     * @param integer $level
1183
     */
1184
    public function setDebugLevel($level = 0)
1185
    {
1186
        $this->do_debug = $level;
1187
    }
1188
1189
    /**
1190
     * Get debug output level.
1191
     *
1192
     * @return integer
1193
     */
1194
    public function getDebugLevel()
1195
    {
1196
        return $this->do_debug;
1197
    }
1198
1199
    /**
1200
     * Set SMTP timeout.
1201
     *
1202
     * @param integer $timeout
1203
     */
1204
    public function setTimeout($timeout = 0)
1205
    {
1206
        $this->Timeout = $timeout;
1207
    }
1208
1209
    /**
1210
     * Get SMTP timeout.
1211
     *
1212
     * @return integer
1213
     */
1214
    public function getTimeout()
1215
    {
1216
        return $this->Timeout;
1217
    }
1218
1219
    /**
1220
     * Reports an error number and string.
1221
     *
1222
     * @param integer $errno The error number returned by PHP.
1223
     * @param string $errmsg The error message returned by PHP.
1224
     * @param string $errfile The file the error occurred in
1225
     * @param integer $errline The line number the error occurred on
1226
     */
1227
    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1228
    {
1229
        $notice = 'Connection failed.';
1230
        $this->setError(
1231
            $notice,
1232
            $errmsg,
1233
            (string)$errno
1234
        );
1235
        $this->edebug(
1236
            "$notice Error #$errno: $errmsg [$errfile line $errline]",
1237
            self::DEBUG_CONNECTION
1238
        );
1239
    }
1240
1241
    /**
1242
     * Extract and return the ID of the last SMTP transaction based on
1243
     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1244
     * Relies on the host providing the ID in response to a DATA command.
1245
     * If no reply has been received yet, it will return null.
1246
     * If no pattern was matched, it will return false.
1247
     * @return bool|null|string
1248
     */
1249
    protected function recordLastTransactionID()
1250
    {
1251
        $reply = $this->getLastReply();
1252
1253
        if (empty($reply)) {
1254
            $this->last_smtp_transaction_id = null;
1255
        } else {
1256
            $this->last_smtp_transaction_id = false;
1257
            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $smtp_transaction_id_pattern exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
1258
                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1259
                    $this->last_smtp_transaction_id = $matches[1];
1260
                }
1261
            }
1262
        }
1263
1264
        return $this->last_smtp_transaction_id;
1265
    }
1266
1267
    /**
1268
     * Get the queue/transaction ID of the last SMTP transaction
1269
     * If no reply has been received yet, it will return null.
1270
     * If no pattern was matched, it will return false.
1271
     * @return bool|null|string
1272
     * @see recordLastTransactionID()
1273
     */
1274
    public function getLastTransactionID()
1275
    {
1276
        return $this->last_smtp_transaction_id;
1277
    }
1278
}
1279