Subversion Repositories web_pages

Rev

Details | Last modification | View Log | RSS feed

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