SMTP   F
last analyzed

Complexity

Total Complexity 150

Size/Duplication

Total Lines 1413
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 150
lcom 1
cbo 1
dl 0
loc 1413
rs 0.8
c 0
b 0
f 0

39 Methods

Rating   Name   Duplication   Size   Complexity  
B edebug() 0 48 8
B connect() 0 48 7
B getSMTPConnection() 0 70 8
A startTLS() 0 27 3
F authenticate() 0 121 25
A hmac() 0 26 3
A connected() 0 20 3
A close() 0 12 2
C data() 0 80 13
A hello() 0 5 2
A sendHello() 0 12 2
B parseHelloFields() 0 35 9
A mail() 0 10 2
A quit() 0 11 3
A recipient() 0 27 5
A reset() 0 4 1
B sendCommand() 0 56 8
A sendAndMail() 0 4 1
A verify() 0 4 1
A noop() 0 4 1
A turn() 0 7 1
A client_send() 0 18 3
A getError() 0 4 1
A getServerExtList() 0 4 1
B getServerExt() 0 22 6
A getLastReply() 0 4 1
C get_lines() 0 83 15
A setVerp() 0 4 1
A getVerp() 0 4 1
A setError() 0 9 1
A setDebugOutput() 0 4 1
A getDebugOutput() 0 4 1
A setDebugLevel() 0 4 1
A getDebugLevel() 0 4 1
A setTimeout() 0 4 1
A getTimeout() 0 4 1
A errorHandler() 0 13 1
A recordLastTransactionID() 0 19 4
A getLastTransactionID() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like SMTP often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SMTP, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * PHPMailer RFC821 SMTP email transport class.
5
 * PHP Version 5.5.
6
 *
7
 * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
8
 *
9
 * @author    Marcus Bointon (Synchro/coolbru) <[email protected]>
10
 * @author    Jim Jagielski (jimjag) <[email protected]>
11
 * @author    Andy Prevost (codeworxtech) <[email protected]>
12
 * @author    Brent R. Matzelle (original founder)
13
 * @copyright 2012 - 2020 Marcus Bointon
14
 * @copyright 2010 - 2012 Jim Jagielski
15
 * @copyright 2004 - 2009 Andy Prevost
16
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
17
 * @note      This program is distributed in the hope that it will be useful - WITHOUT
18
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
 * FITNESS FOR A PARTICULAR PURPOSE.
20
 */
21
22
namespace PHPMailer\PHPMailer;
23
24
/**
25
 * PHPMailer RFC821 SMTP email transport class.
26
 * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
27
 *
28
 * @author Chris Ryan
29
 * @author Marcus Bointon <[email protected]>
30
 */
31
class SMTP
32
{
33
    /**
34
     * The PHPMailer SMTP version number.
35
     *
36
     * @var string
37
     */
38
    const VERSION = '6.2.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 int
51
     */
52
    const DEFAULT_PORT = 25;
53
54
    /**
55
     * The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
56
     * *excluding* a trailing CRLF break.
57
     *
58
     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6
59
     *
60
     * @var int
61
     */
62
    const MAX_LINE_LENGTH = 998;
63
64
    /**
65
     * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
66
     * *including* a trailing CRLF line break.
67
     *
68
     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5
69
     *
70
     * @var int
71
     */
72
    const MAX_REPLY_LENGTH = 512;
73
74
    /**
75
     * Debug level for no output.
76
     *
77
     * @var int
78
     */
79
    const DEBUG_OFF = 0;
80
81
    /**
82
     * Debug level to show client -> server messages.
83
     *
84
     * @var int
85
     */
86
    const DEBUG_CLIENT = 1;
87
88
    /**
89
     * Debug level to show client -> server and server -> client messages.
90
     *
91
     * @var int
92
     */
93
    const DEBUG_SERVER = 2;
94
95
    /**
96
     * Debug level to show connection status, client -> server and server -> client messages.
97
     *
98
     * @var int
99
     */
100
    const DEBUG_CONNECTION = 3;
101
102
    /**
103
     * Debug level to show all messages.
104
     *
105
     * @var int
106
     */
107
    const DEBUG_LOWLEVEL = 4;
108
109
    /**
110
     * Debug output level.
111
     * Options:
112
     * * self::DEBUG_OFF (`0`) No debug output, default
113
     * * self::DEBUG_CLIENT (`1`) Client commands
114
     * * self::DEBUG_SERVER (`2`) Client commands and server responses
115
     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
116
     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
117
     *
118
     * @var int
119
     */
120
    public $do_debug = self::DEBUG_OFF;
121
122
    /**
123
     * How to handle debug output.
124
     * Options:
125
     * * `echo` Output plain-text as-is, appropriate for CLI
126
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
127
     * * `error_log` Output to error log as configured in php.ini
128
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
129
     *
130
     * ```php
131
     * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
132
     * ```
133
     *
134
     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
135
     * level output is used:
136
     *
137
     * ```php
138
     * $mail->Debugoutput = new myPsr3Logger;
139
     * ```
140
     *
141
     * @var string|callable|\Psr\Log\LoggerInterface
142
     */
143
    public $Debugoutput = 'echo';
144
145
    /**
146
     * Whether to use VERP.
147
     *
148
     * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
149
     * @see http://www.postfix.org/VERP_README.html Info on VERP
150
     *
151
     * @var bool
152
     */
153
    public $do_verp = false;
154
155
    /**
156
     * The timeout value for connection, in seconds.
157
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
158
     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
159
     *
160
     * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
161
     *
162
     * @var int
163
     */
164
    public $Timeout = 300;
165
166
    /**
167
     * How long to wait for commands to complete, in seconds.
168
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
169
     *
170
     * @var int
171
     */
172
    public $Timelimit = 300;
173
174
    /**
175
     * Patterns to extract an SMTP transaction id from reply to a DATA command.
176
     * The first capture group in each regex will be used as the ID.
177
     * MS ESMTP returns the message ID, which may not be correct for internal tracking.
178
     *
179
     * @var string[]
180
     */
181
    protected $smtp_transaction_id_patterns = [
182
        'exim' => '/[\d]{3} OK id=(.*)/',
183
        'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/',
184
        'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/',
185
        'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/',
186
        'Amazon_SES' => '/[\d]{3} Ok (.*)/',
187
        'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
188
        'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
189
    ];
190
191
    /**
192
     * The last transaction ID issued in response to a DATA command,
193
     * if one was detected.
194
     *
195
     * @var string|bool|null
196
     */
197
    protected $last_smtp_transaction_id;
198
199
    /**
200
     * The socket for the server connection.
201
     *
202
     * @var ?resource
203
     */
204
    protected $smtp_conn;
205
206
    /**
207
     * Error information, if any, for the last SMTP command.
208
     *
209
     * @var array
210
     */
211
    protected $error = [
212
        'error' => '',
213
        'detail' => '',
214
        'smtp_code' => '',
215
        'smtp_code_ex' => '',
216
    ];
217
218
    /**
219
     * The reply the server sent to us for HELO.
220
     * If null, no HELO string has yet been received.
221
     *
222
     * @var string|null
223
     */
224
    protected $helo_rply;
225
226
    /**
227
     * The set of SMTP extensions sent in reply to EHLO command.
228
     * Indexes of the array are extension names.
229
     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
230
     * represents the server name. In case of HELO it is the only element of the array.
231
     * Other values can be boolean TRUE or an array containing extension options.
232
     * If null, no HELO/EHLO string has yet been received.
233
     *
234
     * @var array|null
235
     */
236
    protected $server_caps;
237
238
    /**
239
     * The most recent reply received from the server.
240
     *
241
     * @var string
242
     */
243
    protected $last_reply = '';
244
245
    /**
246
     * Output debugging info via a user-selected method.
247
     *
248
     * @param string $str   Debug string to output
249
     * @param int    $level The debug level of this message; see DEBUG_* constants
250
     *
251
     * @see SMTP::$Debugoutput
252
     * @see SMTP::$do_debug
253
     */
254
    protected function edebug($str, $level = 0)
255
    {
256
        if ($level > $this->do_debug) {
257
            return;
258
        }
259
        //Is this a PSR-3 logger?
260
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
0 ignored issues
show
Bug introduced by
The class Psr\Log\LoggerInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
261
            $this->Debugoutput->debug($str);
262
263
            return;
264
        }
265
        //Avoid clash with built-in function names
266
        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
267
            call_user_func($this->Debugoutput, $str, $level);
268
269
            return;
270
        }
271
        switch ($this->Debugoutput) {
272
            case 'error_log':
273
                //Don't output, just log
274
                error_log($str);
275
                break;
276
            case 'html':
277
                //Cleans up output a bit for a better looking, HTML-safe output
278
                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
279
                    preg_replace('/[\r\n]+/', '', $str),
280
                    ENT_QUOTES,
281
                    'UTF-8'
282
                ), "<br>\n";
283
                break;
284
            case 'echo':
285
            default:
286
                //Normalize line breaks
287
                $str = preg_replace('/\r\n|\r/m', "\n", $str);
288
                echo gmdate('Y-m-d H:i:s'),
289
                "\t",
290
                    //Trim trailing space
291
                trim(
292
                    //Indent for readability, except for trailing break
293
                    str_replace(
294
                        "\n",
295
                        "\n                   \t                  ",
296
                        trim($str)
297
                    )
298
                ),
299
                "\n";
300
        }
301
    }
302
303
    /**
304
     * Connect to an SMTP server.
305
     *
306
     * @param string $host    SMTP server IP or host name
307
     * @param int    $port    The port number to connect to
308
     * @param int    $timeout How long to wait for the connection to open
309
     * @param array  $options An array of options for stream_context_create()
310
     *
311
     * @return bool
312
     */
313
    public function connect($host, $port = null, $timeout = 30, $options = [])
314
    {
315
        // Clear errors to avoid confusion
316
        $this->setError('');
317
        // Make sure we are __not__ connected
318
        if ($this->connected()) {
319
            // Already connected, generate error
320
            $this->setError('Already connected to a server');
321
322
            return false;
323
        }
324
        if (empty($port)) {
325
            $port = self::DEFAULT_PORT;
326
        }
327
        // Connect to the SMTP server
328
        $this->edebug(
329
            "Connection: opening to $host:$port, timeout=$timeout, options=" .
330
            (count($options) > 0 ? var_export($options, true) : 'array()'),
331
            self::DEBUG_CONNECTION
332
        );
333
334
        $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
335
336
        if ($this->smtp_conn === false) {
337
            //Error info already set inside `getSMTPConnection()`
338
            return false;
339
        }
340
341
        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
342
343
        // Get any announcement
344
        $this->last_reply = $this->get_lines();
345
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
346
        $responseCode = (int)substr($this->last_reply, 0, 3);
347
        if ($responseCode === 220) {
348
            return true;
349
        }
350
        //Anything other than a 220 response means something went wrong
351
        //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
352
        //https://tools.ietf.org/html/rfc5321#section-3.1
353
        if ($responseCode === 554) {
354
            $this->quit();
355
        }
356
        //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
357
        $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
358
        $this->close();
359
        return false;
360
    }
361
362
    /**
363
     * Create connection to the SMTP server.
364
     *
365
     * @param string $host    SMTP server IP or host name
366
     * @param int    $port    The port number to connect to
367
     * @param int    $timeout How long to wait for the connection to open
368
     * @param array  $options An array of options for stream_context_create()
369
     *
370
     * @return false|resource
371
     */
372
    protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
373
    {
374
        static $streamok;
375
        //This is enabled by default since 5.0.0 but some providers disable it
376
        //Check this once and cache the result
377
        if (null === $streamok) {
378
            $streamok = function_exists('stream_socket_client');
379
        }
380
381
        $errno = 0;
382
        $errstr = '';
383
        if ($streamok) {
384
            $socket_context = stream_context_create($options);
385
            set_error_handler([$this, 'errorHandler']);
386
            $connection = stream_socket_client(
387
                $host . ':' . $port,
388
                $errno,
389
                $errstr,
390
                $timeout,
391
                STREAM_CLIENT_CONNECT,
392
                $socket_context
393
            );
394
            restore_error_handler();
395
        } else {
396
            //Fall back to fsockopen which should work in more places, but is missing some features
397
            $this->edebug(
398
                'Connection: stream_socket_client not available, falling back to fsockopen',
399
                self::DEBUG_CONNECTION
400
            );
401
            set_error_handler([$this, 'errorHandler']);
402
            $connection = fsockopen(
403
                $host,
404
                $port,
405
                $errno,
406
                $errstr,
407
                $timeout
408
            );
409
            restore_error_handler();
410
        }
411
412
        // Verify we connected properly
413
        if (!is_resource($connection)) {
414
            $this->setError(
415
                'Failed to connect to server',
416
                '',
417
                (string) $errno,
418
                $errstr
419
            );
420
            $this->edebug(
421
                'SMTP ERROR: ' . $this->error['error']
422
                . ": $errstr ($errno)",
423
                self::DEBUG_CLIENT
424
            );
425
426
            return false;
427
        }
428
429
        // SMTP server can take longer to respond, give longer timeout for first read
430
        // Windows does not have support for this timeout function
431
        if (strpos(PHP_OS, 'WIN') !== 0) {
432
            $max = (int)ini_get('max_execution_time');
433
            // Don't bother if unlimited, or if set_time_limit is disabled
434
            if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
435
                @set_time_limit($timeout);
436
            }
437
            stream_set_timeout($connection, $timeout, 0);
438
        }
439
440
        return $connection;
441
    }
442
443
    /**
444
     * Initiate a TLS (encrypted) session.
445
     *
446
     * @return bool
447
     */
448
    public function startTLS()
449
    {
450
        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
451
            return false;
452
        }
453
454
        //Allow the best TLS version(s) we can
455
        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
456
457
        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
458
        //so add them back in manually if we can
459
        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
460
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
461
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
462
        }
463
464
        // Begin encrypted connection
465
        set_error_handler([$this, 'errorHandler']);
466
        $crypto_ok = stream_socket_enable_crypto(
467
            $this->smtp_conn,
468
            true,
469
            $crypto_method
470
        );
471
        restore_error_handler();
472
473
        return (bool) $crypto_ok;
474
    }
475
476
    /**
477
     * Perform SMTP authentication.
478
     * Must be run after hello().
479
     *
480
     * @see    hello()
481
     *
482
     * @param string $username The user name
483
     * @param string $password The password
484
     * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
485
     * @param OAuth  $OAuth    An optional OAuth instance for XOAUTH2 authentication
486
     *
487
     * @return bool True if successfully authenticated
488
     */
489
    public function authenticate(
490
        $username,
491
        $password,
492
        $authtype = null,
493
        $OAuth = null
494
    ) {
495
        if (!$this->server_caps) {
496
            $this->setError('Authentication is not allowed before HELO/EHLO');
497
498
            return false;
499
        }
500
501
        if (array_key_exists('EHLO', $this->server_caps)) {
502
            // SMTP extensions are available; try to find a proper authentication method
503
            if (!array_key_exists('AUTH', $this->server_caps)) {
504
                $this->setError('Authentication is not allowed at this stage');
505
                // 'at this stage' means that auth may be allowed after the stage changes
506
                // e.g. after STARTTLS
507
508
                return false;
509
            }
510
511
            $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
512
            $this->edebug(
513
                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
514
                self::DEBUG_LOWLEVEL
515
            );
516
517
            //If we have requested a specific auth type, check the server supports it before trying others
518
            if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
519
                $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
520
                $authtype = null;
521
            }
522
523
            if (empty($authtype)) {
524
                //If no auth mechanism is specified, attempt to use these, in this order
525
                //Try CRAM-MD5 first as it's more secure than the others
526
                foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
527
                    if (in_array($method, $this->server_caps['AUTH'], true)) {
528
                        $authtype = $method;
529
                        break;
530
                    }
531
                }
532
                if (empty($authtype)) {
533
                    $this->setError('No supported authentication methods found');
534
535
                    return false;
536
                }
537
                $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
538
            }
539
540
            if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
541
                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
542
543
                return false;
544
            }
545
        } elseif (empty($authtype)) {
546
            $authtype = 'LOGIN';
547
        }
548
        switch ($authtype) {
549
            case 'PLAIN':
550
                // Start authentication
551
                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
552
                    return false;
553
                }
554
                // Send encoded username and password
555
                if (
556
                    !$this->sendCommand(
557
                        'User & Password',
558
                        base64_encode("\0" . $username . "\0" . $password),
559
                        235
560
                    )
561
                ) {
562
                    return false;
563
                }
564
                break;
565
            case 'LOGIN':
566
                // Start authentication
567
                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
568
                    return false;
569
                }
570
                if (!$this->sendCommand('Username', base64_encode($username), 334)) {
571
                    return false;
572
                }
573
                if (!$this->sendCommand('Password', base64_encode($password), 235)) {
574
                    return false;
575
                }
576
                break;
577
            case 'CRAM-MD5':
578
                // Start authentication
579
                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
580
                    return false;
581
                }
582
                // Get the challenge
583
                $challenge = base64_decode(substr($this->last_reply, 4));
584
585
                // Build the response
586
                $response = $username . ' ' . $this->hmac($challenge, $password);
587
588
                // send encoded credentials
589
                return $this->sendCommand('Username', base64_encode($response), 235);
590
            case 'XOAUTH2':
591
                //The OAuth instance must be set up prior to requesting auth.
592
                if (null === $OAuth) {
593
                    return false;
594
                }
595
                $oauth = $OAuth->getOauth64();
596
597
                // Start authentication
598
                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
599
                    return false;
600
                }
601
                break;
602
            default:
603
                $this->setError("Authentication method \"$authtype\" is not supported");
604
605
                return false;
606
        }
607
608
        return true;
609
    }
610
611
    /**
612
     * Calculate an MD5 HMAC hash.
613
     * Works like hash_hmac('md5', $data, $key)
614
     * in case that function is not available.
615
     *
616
     * @param string $data The data to hash
617
     * @param string $key  The key to hash with
618
     *
619
     * @return string
620
     */
621
    protected function hmac($data, $key)
622
    {
623
        if (function_exists('hash_hmac')) {
624
            return hash_hmac('md5', $data, $key);
625
        }
626
627
        // The following borrowed from
628
        // http://php.net/manual/en/function.mhash.php#27225
629
630
        // RFC 2104 HMAC implementation for php.
631
        // Creates an md5 HMAC.
632
        // Eliminates the need to install mhash to compute a HMAC
633
        // by Lance Rushing
634
635
        $bytelen = 64; // byte length for md5
636
        if (strlen($key) > $bytelen) {
637
            $key = pack('H*', md5($key));
638
        }
639
        $key = str_pad($key, $bytelen, chr(0x00));
640
        $ipad = str_pad('', $bytelen, chr(0x36));
641
        $opad = str_pad('', $bytelen, chr(0x5c));
642
        $k_ipad = $key ^ $ipad;
643
        $k_opad = $key ^ $opad;
644
645
        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
646
    }
647
648
    /**
649
     * Check connection state.
650
     *
651
     * @return bool True if connected
652
     */
653
    public function connected()
654
    {
655
        if (is_resource($this->smtp_conn)) {
656
            $sock_status = stream_get_meta_data($this->smtp_conn);
657
            if ($sock_status['eof']) {
658
                // The socket is valid but we are not connected
659
                $this->edebug(
660
                    'SMTP NOTICE: EOF caught while checking if connected',
661
                    self::DEBUG_CLIENT
662
                );
663
                $this->close();
664
665
                return false;
666
            }
667
668
            return true; // everything looks good
669
        }
670
671
        return false;
672
    }
673
674
    /**
675
     * Close the socket and clean up the state of the class.
676
     * Don't use this function without first trying to use QUIT.
677
     *
678
     * @see quit()
679
     */
680
    public function close()
681
    {
682
        $this->setError('');
683
        $this->server_caps = null;
684
        $this->helo_rply = null;
685
        if (is_resource($this->smtp_conn)) {
686
            // close the connection and cleanup
687
            fclose($this->smtp_conn);
688
            $this->smtp_conn = null; //Makes for cleaner serialization
689
            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
690
        }
691
    }
692
693
    /**
694
     * Send an SMTP DATA command.
695
     * Issues a data command and sends the msg_data to the server,
696
     * finializing the mail transaction. $msg_data is the message
697
     * that is to be send with the headers. Each header needs to be
698
     * on a single line followed by a <CRLF> with the message headers
699
     * and the message body being separated by an additional <CRLF>.
700
     * Implements RFC 821: DATA <CRLF>.
701
     *
702
     * @param string $msg_data Message data to send
703
     *
704
     * @return bool
705
     */
706
    public function data($msg_data)
707
    {
708
        //This will use the standard timelimit
709
        if (!$this->sendCommand('DATA', 'DATA', 354)) {
710
            return false;
711
        }
712
713
        /* The server is ready to accept data!
714
         * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
715
         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
716
         * smaller lines to fit within the limit.
717
         * We will also look for lines that start with a '.' and prepend an additional '.'.
718
         * NOTE: this does not count towards line-length limit.
719
         */
720
721
        // Normalize line breaks before exploding
722
        $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
723
724
        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
725
         * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
726
         * process all lines before a blank line as headers.
727
         */
728
729
        $field = substr($lines[0], 0, strpos($lines[0], ':'));
730
        $in_headers = false;
731
        if (!empty($field) && strpos($field, ' ') === false) {
732
            $in_headers = true;
733
        }
734
735
        foreach ($lines as $line) {
736
            $lines_out = [];
737
            if ($in_headers && $line === '') {
738
                $in_headers = false;
739
            }
740
            //Break this line up into several smaller lines if it's too long
741
            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
742
            while (isset($line[self::MAX_LINE_LENGTH])) {
743
                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
744
                //so as to avoid breaking in the middle of a word
745
                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
746
                //Deliberately matches both false and 0
747
                if (!$pos) {
748
                    //No nice break found, add a hard break
749
                    $pos = self::MAX_LINE_LENGTH - 1;
750
                    $lines_out[] = substr($line, 0, $pos);
751
                    $line = substr($line, $pos);
752
                } else {
753
                    //Break at the found point
754
                    $lines_out[] = substr($line, 0, $pos);
755
                    //Move along by the amount we dealt with
756
                    $line = substr($line, $pos + 1);
757
                }
758
                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
759
                if ($in_headers) {
760
                    $line = "\t" . $line;
761
                }
762
            }
763
            $lines_out[] = $line;
764
765
            //Send the lines to the server
766
            foreach ($lines_out as $line_out) {
767
                //RFC2821 section 4.5.2
768
                if (!empty($line_out) && $line_out[0] === '.') {
769
                    $line_out = '.' . $line_out;
770
                }
771
                $this->client_send($line_out . static::LE, 'DATA');
772
            }
773
        }
774
775
        //Message data has been sent, complete the command
776
        //Increase timelimit for end of DATA command
777
        $savetimelimit = $this->Timelimit;
778
        $this->Timelimit *= 2;
779
        $result = $this->sendCommand('DATA END', '.', 250);
780
        $this->recordLastTransactionID();
781
        //Restore timelimit
782
        $this->Timelimit = $savetimelimit;
783
784
        return $result;
785
    }
786
787
    /**
788
     * Send an SMTP HELO or EHLO command.
789
     * Used to identify the sending server to the receiving server.
790
     * This makes sure that client and server are in a known state.
791
     * Implements RFC 821: HELO <SP> <domain> <CRLF>
792
     * and RFC 2821 EHLO.
793
     *
794
     * @param string $host The host name or IP to connect to
795
     *
796
     * @return bool
797
     */
798
    public function hello($host = '')
799
    {
800
        //Try extended hello first (RFC 2821)
801
        return $this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host);
802
    }
803
804
    /**
805
     * Send an SMTP HELO or EHLO command.
806
     * Low-level implementation used by hello().
807
     *
808
     * @param string $hello The HELO string
809
     * @param string $host  The hostname to say we are
810
     *
811
     * @return bool
812
     *
813
     * @see hello()
814
     */
815
    protected function sendHello($hello, $host)
816
    {
817
        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
818
        $this->helo_rply = $this->last_reply;
819
        if ($noerror) {
820
            $this->parseHelloFields($hello);
821
        } else {
822
            $this->server_caps = null;
823
        }
824
825
        return $noerror;
826
    }
827
828
    /**
829
     * Parse a reply to HELO/EHLO command to discover server extensions.
830
     * In case of HELO, the only parameter that can be discovered is a server name.
831
     *
832
     * @param string $type `HELO` or `EHLO`
833
     */
834
    protected function parseHelloFields($type)
835
    {
836
        $this->server_caps = [];
837
        $lines = explode("\n", $this->helo_rply);
838
839
        foreach ($lines as $n => $s) {
840
            //First 4 chars contain response code followed by - or space
841
            $s = trim(substr($s, 4));
842
            if (empty($s)) {
843
                continue;
844
            }
845
            $fields = explode(' ', $s);
846
            if (!empty($fields)) {
847
                if (!$n) {
848
                    $name = $type;
849
                    $fields = $fields[0];
850
                } else {
851
                    $name = array_shift($fields);
852
                    switch ($name) {
853
                        case 'SIZE':
854
                            $fields = ($fields ? $fields[0] : 0);
855
                            break;
856
                        case 'AUTH':
857
                            if (!is_array($fields)) {
858
                                $fields = [];
859
                            }
860
                            break;
861
                        default:
862
                            $fields = true;
863
                    }
864
                }
865
                $this->server_caps[$name] = $fields;
866
            }
867
        }
868
    }
869
870
    /**
871
     * Send an SMTP MAIL command.
872
     * Starts a mail transaction from the email address specified in
873
     * $from. Returns true if successful or false otherwise. If True
874
     * the mail transaction is started and then one or more recipient
875
     * commands may be called followed by a data command.
876
     * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
877
     *
878
     * @param string $from Source address of this message
879
     *
880
     * @return bool
881
     */
882
    public function mail($from)
883
    {
884
        $useVerp = ($this->do_verp ? ' XVERP' : '');
885
886
        return $this->sendCommand(
887
            'MAIL FROM',
888
            'MAIL FROM:<' . $from . '>' . $useVerp,
889
            250
890
        );
891
    }
892
893
    /**
894
     * Send an SMTP QUIT command.
895
     * Closes the socket if there is no error or the $close_on_error argument is true.
896
     * Implements from RFC 821: QUIT <CRLF>.
897
     *
898
     * @param bool $close_on_error Should the connection close if an error occurs?
899
     *
900
     * @return bool
901
     */
902
    public function quit($close_on_error = true)
903
    {
904
        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
905
        $err = $this->error; //Save any error
906
        if ($noerror || $close_on_error) {
907
            $this->close();
908
            $this->error = $err; //Restore any error from the quit command
909
        }
910
911
        return $noerror;
912
    }
913
914
    /**
915
     * Send an SMTP RCPT command.
916
     * Sets the TO argument to $toaddr.
917
     * Returns true if the recipient was accepted false if it was rejected.
918
     * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
919
     *
920
     * @param string $address The address the message is being sent to
921
     * @param string $dsn     Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
922
     *                        or DELAY. If you specify NEVER all other notifications are ignored.
923
     *
924
     * @return bool
925
     */
926
    public function recipient($address, $dsn = '')
927
    {
928
        if (empty($dsn)) {
929
            $rcpt = 'RCPT TO:<' . $address . '>';
930
        } else {
931
            $dsn = strtoupper($dsn);
932
            $notify = [];
933
934
            if (strpos($dsn, 'NEVER') !== false) {
935
                $notify[] = 'NEVER';
936
            } else {
937
                foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
938
                    if (strpos($dsn, $value) !== false) {
939
                        $notify[] = $value;
940
                    }
941
                }
942
            }
943
944
            $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
945
        }
946
947
        return $this->sendCommand(
948
            'RCPT TO',
949
            $rcpt,
950
            [250, 251]
951
        );
952
    }
953
954
    /**
955
     * Send an SMTP RSET command.
956
     * Abort any transaction that is currently in progress.
957
     * Implements RFC 821: RSET <CRLF>.
958
     *
959
     * @return bool True on success
960
     */
961
    public function reset()
962
    {
963
        return $this->sendCommand('RSET', 'RSET', 250);
964
    }
965
966
    /**
967
     * Send a command to an SMTP server and check its return code.
968
     *
969
     * @param string    $command       The command name - not sent to the server
970
     * @param string    $commandstring The actual command to send
971
     * @param int|array $expect        One or more expected integer success codes
972
     *
973
     * @return bool True on success
974
     */
975
    protected function sendCommand($command, $commandstring, $expect)
976
    {
977
        if (!$this->connected()) {
978
            $this->setError("Called $command without being connected");
979
980
            return false;
981
        }
982
        //Reject line breaks in all commands
983
        if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
984
            $this->setError("Command '$command' contained line breaks");
985
986
            return false;
987
        }
988
        $this->client_send($commandstring . static::LE, $command);
989
990
        $this->last_reply = $this->get_lines();
991
        // Fetch SMTP code and possible error code explanation
992
        $matches = [];
993
        if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
994
            $code = (int) $matches[1];
995
            $code_ex = (count($matches) > 2 ? $matches[2] : null);
996
            // Cut off error code from each response line
997
            $detail = preg_replace(
998
                "/{$code}[ -]" .
999
                ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
1000
                '',
1001
                $this->last_reply
1002
            );
1003
        } else {
1004
            // Fall back to simple parsing if regex fails
1005
            $code = (int) substr($this->last_reply, 0, 3);
1006
            $code_ex = null;
1007
            $detail = substr($this->last_reply, 4);
1008
        }
1009
1010
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
1011
1012
        if (!in_array($code, (array) $expect, true)) {
1013
            $this->setError(
1014
                "$command command failed",
1015
                $detail,
1016
                $code,
1017
                $code_ex
1018
            );
1019
            $this->edebug(
1020
                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
1021
                self::DEBUG_CLIENT
1022
            );
1023
1024
            return false;
1025
        }
1026
1027
        $this->setError('');
1028
1029
        return true;
1030
    }
1031
1032
    /**
1033
     * Send an SMTP SAML command.
1034
     * Starts a mail transaction from the email address specified in $from.
1035
     * Returns true if successful or false otherwise. If True
1036
     * the mail transaction is started and then one or more recipient
1037
     * commands may be called followed by a data command. This command
1038
     * will send the message to the users terminal if they are logged
1039
     * in and send them an email.
1040
     * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
1041
     *
1042
     * @param string $from The address the message is from
1043
     *
1044
     * @return bool
1045
     */
1046
    public function sendAndMail($from)
1047
    {
1048
        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
1049
    }
1050
1051
    /**
1052
     * Send an SMTP VRFY command.
1053
     *
1054
     * @param string $name The name to verify
1055
     *
1056
     * @return bool
1057
     */
1058
    public function verify($name)
1059
    {
1060
        return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
1061
    }
1062
1063
    /**
1064
     * Send an SMTP NOOP command.
1065
     * Used to keep keep-alives alive, doesn't actually do anything.
1066
     *
1067
     * @return bool
1068
     */
1069
    public function noop()
1070
    {
1071
        return $this->sendCommand('NOOP', 'NOOP', 250);
1072
    }
1073
1074
    /**
1075
     * Send an SMTP TURN command.
1076
     * This is an optional command for SMTP that this class does not support.
1077
     * This method is here to make the RFC821 Definition complete for this class
1078
     * and _may_ be implemented in future.
1079
     * Implements from RFC 821: TURN <CRLF>.
1080
     *
1081
     * @return bool
1082
     */
1083
    public function turn()
1084
    {
1085
        $this->setError('The SMTP TURN command is not implemented');
1086
        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
1087
1088
        return false;
1089
    }
1090
1091
    /**
1092
     * Send raw data to the server.
1093
     *
1094
     * @param string $data    The data to send
1095
     * @param string $command Optionally, the command this is part of, used only for controlling debug output
1096
     *
1097
     * @return int|bool The number of bytes sent to the server or false on error
1098
     */
1099
    public function client_send($data, $command = '')
1100
    {
1101
        //If SMTP transcripts are left enabled, or debug output is posted online
1102
        //it can leak credentials, so hide credentials in all but lowest level
1103
        if (
1104
            self::DEBUG_LOWLEVEL > $this->do_debug &&
1105
            in_array($command, ['User & Password', 'Username', 'Password'], true)
1106
        ) {
1107
            $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
1108
        } else {
1109
            $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
1110
        }
1111
        set_error_handler([$this, 'errorHandler']);
1112
        $result = fwrite($this->smtp_conn, $data);
1113
        restore_error_handler();
1114
1115
        return $result;
1116
    }
1117
1118
    /**
1119
     * Get the latest error.
1120
     *
1121
     * @return array
1122
     */
1123
    public function getError()
1124
    {
1125
        return $this->error;
1126
    }
1127
1128
    /**
1129
     * Get SMTP extensions available on the server.
1130
     *
1131
     * @return array|null
1132
     */
1133
    public function getServerExtList()
1134
    {
1135
        return $this->server_caps;
1136
    }
1137
1138
    /**
1139
     * Get metadata about the SMTP server from its HELO/EHLO response.
1140
     * The method works in three ways, dependent on argument value and current state:
1141
     *   1. HELO/EHLO has not been sent - returns null and populates $this->error.
1142
     *   2. HELO has been sent -
1143
     *     $name == 'HELO': returns server name
1144
     *     $name == 'EHLO': returns boolean false
1145
     *     $name == any other string: returns null and populates $this->error
1146
     *   3. EHLO has been sent -
1147
     *     $name == 'HELO'|'EHLO': returns the server name
1148
     *     $name == any other string: if extension $name exists, returns True
1149
     *       or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
1150
     *
1151
     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
1152
     *
1153
     * @return string|bool|null
1154
     */
1155
    public function getServerExt($name)
1156
    {
1157
        if (!$this->server_caps) {
1158
            $this->setError('No HELO/EHLO was sent');
1159
1160
            return;
1161
        }
1162
1163
        if (!array_key_exists($name, $this->server_caps)) {
1164
            if ('HELO' === $name) {
1165
                return $this->server_caps['EHLO'];
1166
            }
1167
            if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
1168
                return false;
1169
            }
1170
            $this->setError('HELO handshake was used; No information about server extensions available');
1171
1172
            return;
1173
        }
1174
1175
        return $this->server_caps[$name];
1176
    }
1177
1178
    /**
1179
     * Get the last reply from the server.
1180
     *
1181
     * @return string
1182
     */
1183
    public function getLastReply()
1184
    {
1185
        return $this->last_reply;
1186
    }
1187
1188
    /**
1189
     * Read the SMTP server's response.
1190
     * Either before eof or socket timeout occurs on the operation.
1191
     * With SMTP we can tell if we have more lines to read if the
1192
     * 4th character is '-' symbol. If it is a space then we don't
1193
     * need to read anything else.
1194
     *
1195
     * @return string
1196
     */
1197
    protected function get_lines()
1198
    {
1199
        // If the connection is bad, give up straight away
1200
        if (!is_resource($this->smtp_conn)) {
1201
            return '';
1202
        }
1203
        $data = '';
1204
        $endtime = 0;
1205
        stream_set_timeout($this->smtp_conn, $this->Timeout);
1206
        if ($this->Timelimit > 0) {
1207
            $endtime = time() + $this->Timelimit;
1208
        }
1209
        $selR = [$this->smtp_conn];
1210
        $selW = null;
1211
        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1212
            //Must pass vars in here as params are by reference
1213
            //solution for signals inspired by https://github.com/symfony/symfony/pull/6540
1214
            set_error_handler([$this, 'errorHandler']);
1215
            $n = stream_select($selR, $selW, $selW, $this->Timelimit);
1216
            restore_error_handler();
1217
1218
            if ($n === false) {
1219
                $message = $this->getError()['detail'];
1220
1221
                $this->edebug(
1222
                    'SMTP -> get_lines(): select failed (' . $message . ')',
1223
                    self::DEBUG_LOWLEVEL
1224
                );
1225
1226
                //stream_select returns false when the `select` system call is interrupted
1227
                //by an incoming signal, try the select again
1228
                if (stripos($message, 'interrupted system call') !== false) {
1229
                    $this->edebug(
1230
                        'SMTP -> get_lines(): retrying stream_select',
1231
                        self::DEBUG_LOWLEVEL
1232
                    );
1233
                    $this->setError('');
1234
                    continue;
1235
                }
1236
1237
                break;
1238
            }
1239
1240
            if (!$n) {
1241
                $this->edebug(
1242
                    'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
1243
                    self::DEBUG_LOWLEVEL
1244
                );
1245
                break;
1246
            }
1247
1248
            //Deliberate noise suppression - errors are handled afterwards
1249
            $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
1250
            $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
1251
            $data .= $str;
1252
            // If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
1253
            // or 4th character is a space or a line break char, we are done reading, break the loop.
1254
            // String array access is a significant micro-optimisation over strlen
1255
            if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
1256
                break;
1257
            }
1258
            // Timed-out? Log and break
1259
            $info = stream_get_meta_data($this->smtp_conn);
1260
            if ($info['timed_out']) {
1261
                $this->edebug(
1262
                    'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
1263
                    self::DEBUG_LOWLEVEL
1264
                );
1265
                break;
1266
            }
1267
            // Now check if reads took too long
1268
            if ($endtime && time() > $endtime) {
1269
                $this->edebug(
1270
                    'SMTP -> get_lines(): timelimit reached (' .
1271
                    $this->Timelimit . ' sec)',
1272
                    self::DEBUG_LOWLEVEL
1273
                );
1274
                break;
1275
            }
1276
        }
1277
1278
        return $data;
1279
    }
1280
1281
    /**
1282
     * Enable or disable VERP address generation.
1283
     *
1284
     * @param bool $enabled
1285
     */
1286
    public function setVerp($enabled = false)
1287
    {
1288
        $this->do_verp = $enabled;
1289
    }
1290
1291
    /**
1292
     * Get VERP address generation mode.
1293
     *
1294
     * @return bool
1295
     */
1296
    public function getVerp()
1297
    {
1298
        return $this->do_verp;
1299
    }
1300
1301
    /**
1302
     * Set error messages and codes.
1303
     *
1304
     * @param string $message      The error message
1305
     * @param string $detail       Further detail on the error
1306
     * @param string $smtp_code    An associated SMTP error code
1307
     * @param string $smtp_code_ex Extended SMTP code
1308
     */
1309
    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1310
    {
1311
        $this->error = [
1312
            'error' => $message,
1313
            'detail' => $detail,
1314
            'smtp_code' => $smtp_code,
1315
            'smtp_code_ex' => $smtp_code_ex,
1316
        ];
1317
    }
1318
1319
    /**
1320
     * Set debug output method.
1321
     *
1322
     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
1323
     */
1324
    public function setDebugOutput($method = 'echo')
1325
    {
1326
        $this->Debugoutput = $method;
1327
    }
1328
1329
    /**
1330
     * Get debug output method.
1331
     *
1332
     * @return string
1333
     */
1334
    public function getDebugOutput()
1335
    {
1336
        return $this->Debugoutput;
1337
    }
1338
1339
    /**
1340
     * Set debug output level.
1341
     *
1342
     * @param int $level
1343
     */
1344
    public function setDebugLevel($level = 0)
1345
    {
1346
        $this->do_debug = $level;
1347
    }
1348
1349
    /**
1350
     * Get debug output level.
1351
     *
1352
     * @return int
1353
     */
1354
    public function getDebugLevel()
1355
    {
1356
        return $this->do_debug;
1357
    }
1358
1359
    /**
1360
     * Set SMTP timeout.
1361
     *
1362
     * @param int $timeout The timeout duration in seconds
1363
     */
1364
    public function setTimeout($timeout = 0)
1365
    {
1366
        $this->Timeout = $timeout;
1367
    }
1368
1369
    /**
1370
     * Get SMTP timeout.
1371
     *
1372
     * @return int
1373
     */
1374
    public function getTimeout()
1375
    {
1376
        return $this->Timeout;
1377
    }
1378
1379
    /**
1380
     * Reports an error number and string.
1381
     *
1382
     * @param int    $errno   The error number returned by PHP
1383
     * @param string $errmsg  The error message returned by PHP
1384
     * @param string $errfile The file the error occurred in
1385
     * @param int    $errline The line number the error occurred on
1386
     */
1387
    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1388
    {
1389
        $notice = 'Connection failed.';
1390
        $this->setError(
1391
            $notice,
1392
            $errmsg,
1393
            (string) $errno
1394
        );
1395
        $this->edebug(
1396
            "$notice Error #$errno: $errmsg [$errfile line $errline]",
1397
            self::DEBUG_CONNECTION
1398
        );
1399
    }
1400
1401
    /**
1402
     * Extract and return the ID of the last SMTP transaction based on
1403
     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1404
     * Relies on the host providing the ID in response to a DATA command.
1405
     * If no reply has been received yet, it will return null.
1406
     * If no pattern was matched, it will return false.
1407
     *
1408
     * @return bool|string|null
1409
     */
1410
    protected function recordLastTransactionID()
1411
    {
1412
        $reply = $this->getLastReply();
1413
1414
        if (empty($reply)) {
1415
            $this->last_smtp_transaction_id = null;
1416
        } else {
1417
            $this->last_smtp_transaction_id = false;
1418
            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1419
                $matches = [];
1420
                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1421
                    $this->last_smtp_transaction_id = trim($matches[1]);
1422
                    break;
1423
                }
1424
            }
1425
        }
1426
1427
        return $this->last_smtp_transaction_id;
1428
    }
1429
1430
    /**
1431
     * Get the queue/transaction ID of the last SMTP transaction
1432
     * If no reply has been received yet, it will return null.
1433
     * If no pattern was matched, it will return false.
1434
     *
1435
     * @return bool|string|null
1436
     *
1437
     * @see recordLastTransactionID()
1438
     */
1439
    public function getLastTransactionID()
1440
    {
1441
        return $this->last_smtp_transaction_id;
1442
    }
1443
}
1444