SMTP::setVerp()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 0
f 0
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   https://www.gnu.org/licenses/old-licenses/lgpl-2.1.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.9.3';
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://www.rfc-editor.org/rfc/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://www.rfc-editor.org/rfc/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
0 ignored issues
show
Bug introduced by
The type Psr\Log\LoggerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
149
     */
150
    public $Debugoutput = 'echo';
151
152
    /**
153
     * Whether to use VERP.
154
     *
155
     * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
156
     * @see https://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 https://www.rfc-editor.org/rfc/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
            //Remove trailing line breaks potentially added by calls to SMTP::client_send()
284
            $this->Debugoutput->debug(rtrim($str, "\r\n"));
285
286
            return;
287
        }
288
        //Avoid clash with built-in function names
289
        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
290
            call_user_func($this->Debugoutput, $str, $level);
291
292
            return;
293
        }
294
        switch ($this->Debugoutput) {
295
            case 'error_log':
296
                //Don't output, just log
297
                /** @noinspection ForgottenDebugOutputInspection */
298
                error_log($str);
299
                break;
300
            case 'html':
301
                //Cleans up output a bit for a better looking, HTML-safe output
302
                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
303
                    preg_replace('/[\r\n]+/', '', $str),
304
                    ENT_QUOTES,
305
                    'UTF-8'
306
                ), "<br>\n";
307
                break;
308
            case 'echo':
309
            default:
310
                //Normalize line breaks
311
                $str = preg_replace('/\r\n|\r/m', "\n", $str);
312
                echo gmdate('Y-m-d H:i:s'),
313
                "\t",
314
                    //Trim trailing space
315
                trim(
316
                    //Indent for readability, except for trailing break
317
                    str_replace(
318
                        "\n",
319
                        "\n                   \t                  ",
320
                        trim($str)
321
                    )
322
                ),
323
                "\n";
324
        }
325
    }
326
327
    /**
328
     * Connect to an SMTP server.
329
     *
330
     * @param string $host    SMTP server IP or host name
331
     * @param int    $port    The port number to connect to
332
     * @param int    $timeout How long to wait for the connection to open
333
     * @param array  $options An array of options for stream_context_create()
334
     *
335
     * @return bool
336
     */
337
    public function connect($host, $port = null, $timeout = 30, $options = [])
338
    {
339
        //Clear errors to avoid confusion
340
        $this->setError('');
341
        //Make sure we are __not__ connected
342
        if ($this->connected()) {
343
            //Already connected, generate error
344
            $this->setError('Already connected to a server');
345
346
            return false;
347
        }
348
        if (empty($port)) {
349
            $port = self::DEFAULT_PORT;
350
        }
351
        //Connect to the SMTP server
352
        $this->edebug(
353
            "Connection: opening to $host:$port, timeout=$timeout, options=" .
354
            (count($options) > 0 ? var_export($options, true) : 'array()'),
355
            self::DEBUG_CONNECTION
356
        );
357
358
        $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getSMTPConnection...rt, $timeout, $options) can also be of type false. However, the property $smtp_conn is declared as type null|resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
359
360
        if ($this->smtp_conn === false) {
361
            //Error info already set inside `getSMTPConnection()`
362
            return false;
363
        }
364
365
        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
366
367
        //Get any announcement
368
        $this->last_reply = $this->get_lines();
369
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
370
        $responseCode = (int)substr($this->last_reply, 0, 3);
371
        if ($responseCode === 220) {
372
            return true;
373
        }
374
        //Anything other than a 220 response means something went wrong
375
        //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
376
        //https://www.rfc-editor.org/rfc/rfc5321#section-3.1
377
        if ($responseCode === 554) {
378
            $this->quit();
379
        }
380
        //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
381
        $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
382
        $this->close();
383
        return false;
384
    }
385
386
    /**
387
     * Create connection to the SMTP server.
388
     *
389
     * @param string $host    SMTP server IP or host name
390
     * @param int    $port    The port number to connect to
391
     * @param int    $timeout How long to wait for the connection to open
392
     * @param array  $options An array of options for stream_context_create()
393
     *
394
     * @return false|resource
395
     */
396
    protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
397
    {
398
        static $streamok;
399
        //This is enabled by default since 5.0.0 but some providers disable it
400
        //Check this once and cache the result
401
        if (null === $streamok) {
402
            $streamok = function_exists('stream_socket_client');
403
        }
404
405
        $errno = 0;
406
        $errstr = '';
407
        if ($streamok) {
408
            $socket_context = stream_context_create($options);
409
            set_error_handler(function () {
410
                call_user_func_array([$this, 'errorHandler'], func_get_args());
411
            });
412
            $connection = stream_socket_client(
413
                $host . ':' . $port,
414
                $errno,
415
                $errstr,
416
                $timeout,
417
                STREAM_CLIENT_CONNECT,
418
                $socket_context
419
            );
420
        } else {
421
            //Fall back to fsockopen which should work in more places, but is missing some features
422
            $this->edebug(
423
                'Connection: stream_socket_client not available, falling back to fsockopen',
424
                self::DEBUG_CONNECTION
425
            );
426
            set_error_handler(function () {
427
                call_user_func_array([$this, 'errorHandler'], func_get_args());
428
            });
429
            $connection = fsockopen(
430
                $host,
431
                $port,
0 ignored issues
show
Bug introduced by
It seems like $port can also be of type null; however, parameter $port of fsockopen() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

431
                /** @scrutinizer ignore-type */ $port,
Loading history...
432
                $errno,
433
                $errstr,
434
                $timeout
435
            );
436
        }
437
        restore_error_handler();
438
439
        //Verify we connected properly
440
        if (!is_resource($connection)) {
441
            $this->setError(
442
                'Failed to connect to server',
443
                '',
444
                (string) $errno,
445
                $errstr
446
            );
447
            $this->edebug(
448
                'SMTP ERROR: ' . $this->error['error']
449
                . ": $errstr ($errno)",
450
                self::DEBUG_CLIENT
451
            );
452
453
            return false;
454
        }
455
456
        //SMTP server can take longer to respond, give longer timeout for first read
457
        //Windows does not have support for this timeout function
458
        if (strpos(PHP_OS, 'WIN') !== 0) {
459
            $max = (int)ini_get('max_execution_time');
460
            //Don't bother if unlimited, or if set_time_limit is disabled
461
            if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
462
                @set_time_limit($timeout);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for set_time_limit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

462
                /** @scrutinizer ignore-unhandled */ @set_time_limit($timeout);

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
463
            }
464
            stream_set_timeout($connection, $timeout, 0);
465
        }
466
467
        return $connection;
468
    }
469
470
    /**
471
     * Initiate a TLS (encrypted) session.
472
     *
473
     * @return bool
474
     */
475
    public function startTLS()
476
    {
477
        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
478
            return false;
479
        }
480
481
        //Allow the best TLS version(s) we can
482
        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
483
484
        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
485
        //so add them back in manually if we can
486
        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
487
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
488
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
489
        }
490
491
        //Begin encrypted connection
492
            set_error_handler(function () {
493
                call_user_func_array([$this, 'errorHandler'], func_get_args());
494
            });
495
        $crypto_ok = stream_socket_enable_crypto(
496
            $this->smtp_conn,
497
            true,
498
            $crypto_method
499
        );
500
        restore_error_handler();
501
502
        return (bool) $crypto_ok;
503
    }
504
505
    /**
506
     * Perform SMTP authentication.
507
     * Must be run after hello().
508
     *
509
     * @see    hello()
510
     *
511
     * @param string $username The user name
512
     * @param string $password The password
513
     * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
514
     * @param OAuthTokenProvider $OAuth An optional OAuthTokenProvider instance for XOAUTH2 authentication
515
     *
516
     * @return bool True if successfully authenticated
517
     */
518
    public function authenticate(
519
        $username,
520
        $password,
521
        $authtype = null,
522
        $OAuth = null
523
    ) {
524
        if (!$this->server_caps) {
525
            $this->setError('Authentication is not allowed before HELO/EHLO');
526
527
            return false;
528
        }
529
530
        if (array_key_exists('EHLO', $this->server_caps)) {
531
            //SMTP extensions are available; try to find a proper authentication method
532
            if (!array_key_exists('AUTH', $this->server_caps)) {
533
                $this->setError('Authentication is not allowed at this stage');
534
                //'at this stage' means that auth may be allowed after the stage changes
535
                //e.g. after STARTTLS
536
537
                return false;
538
            }
539
540
            $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
541
            $this->edebug(
542
                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
543
                self::DEBUG_LOWLEVEL
544
            );
545
546
            //If we have requested a specific auth type, check the server supports it before trying others
547
            if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
548
                $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
549
                $authtype = null;
550
            }
551
552
            if (empty($authtype)) {
553
                //If no auth mechanism is specified, attempt to use these, in this order
554
                //Try CRAM-MD5 first as it's more secure than the others
555
                foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
556
                    if (in_array($method, $this->server_caps['AUTH'], true)) {
557
                        $authtype = $method;
558
                        break;
559
                    }
560
                }
561
                if (empty($authtype)) {
562
                    $this->setError('No supported authentication methods found');
563
564
                    return false;
565
                }
566
                $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
567
            }
568
569
            if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
570
                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
571
572
                return false;
573
            }
574
        } elseif (empty($authtype)) {
575
            $authtype = 'LOGIN';
576
        }
577
        switch ($authtype) {
578
            case 'PLAIN':
579
                //Start authentication
580
                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
581
                    return false;
582
                }
583
                //Send encoded username and password
584
                if (
585
                    //Format from https://www.rfc-editor.org/rfc/rfc4616#section-2
586
                    //We skip the first field (it's forgery), so the string starts with a null byte
587
                    !$this->sendCommand(
588
                        'User & Password',
589
                        base64_encode("\0" . $username . "\0" . $password),
590
                        235
591
                    )
592
                ) {
593
                    return false;
594
                }
595
                break;
596
            case 'LOGIN':
597
                //Start authentication
598
                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
599
                    return false;
600
                }
601
                if (!$this->sendCommand('Username', base64_encode($username), 334)) {
602
                    return false;
603
                }
604
                if (!$this->sendCommand('Password', base64_encode($password), 235)) {
605
                    return false;
606
                }
607
                break;
608
            case 'CRAM-MD5':
609
                //Start authentication
610
                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
611
                    return false;
612
                }
613
                //Get the challenge
614
                $challenge = base64_decode(substr($this->last_reply, 4));
615
616
                //Build the response
617
                $response = $username . ' ' . $this->hmac($challenge, $password);
618
619
                //send encoded credentials
620
                return $this->sendCommand('Username', base64_encode($response), 235);
621
            case 'XOAUTH2':
622
                //The OAuth instance must be set up prior to requesting auth.
623
                if (null === $OAuth) {
624
                    return false;
625
                }
626
                $oauth = $OAuth->getOauth64();
627
628
                //Start authentication
629
                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
630
                    return false;
631
                }
632
                break;
633
            default:
634
                $this->setError("Authentication method \"$authtype\" is not supported");
635
636
                return false;
637
        }
638
639
        return true;
640
    }
641
642
    /**
643
     * Calculate an MD5 HMAC hash.
644
     * Works like hash_hmac('md5', $data, $key)
645
     * in case that function is not available.
646
     *
647
     * @param string $data The data to hash
648
     * @param string $key  The key to hash with
649
     *
650
     * @return string
651
     */
652
    protected function hmac($data, $key)
653
    {
654
        if (function_exists('hash_hmac')) {
655
            return hash_hmac('md5', $data, $key);
656
        }
657
658
        //The following borrowed from
659
        //https://www.php.net/manual/en/function.mhash.php#27225
660
661
        //RFC 2104 HMAC implementation for php.
662
        //Creates an md5 HMAC.
663
        //Eliminates the need to install mhash to compute a HMAC
664
        //by Lance Rushing
665
666
        $bytelen = 64; //byte length for md5
667
        if (strlen($key) > $bytelen) {
668
            $key = pack('H*', md5($key));
669
        }
670
        $key = str_pad($key, $bytelen, chr(0x00));
671
        $ipad = str_pad('', $bytelen, chr(0x36));
672
        $opad = str_pad('', $bytelen, chr(0x5c));
673
        $k_ipad = $key ^ $ipad;
674
        $k_opad = $key ^ $opad;
675
676
        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
677
    }
678
679
    /**
680
     * Check connection state.
681
     *
682
     * @return bool True if connected
683
     */
684
    public function connected()
685
    {
686
        if (is_resource($this->smtp_conn)) {
687
            $sock_status = stream_get_meta_data($this->smtp_conn);
688
            if ($sock_status['eof']) {
689
                //The socket is valid but we are not connected
690
                $this->edebug(
691
                    'SMTP NOTICE: EOF caught while checking if connected',
692
                    self::DEBUG_CLIENT
693
                );
694
                $this->close();
695
696
                return false;
697
            }
698
699
            return true; //everything looks good
700
        }
701
702
        return false;
703
    }
704
705
    /**
706
     * Close the socket and clean up the state of the class.
707
     * Don't use this function without first trying to use QUIT.
708
     *
709
     * @see quit()
710
     */
711
    public function close()
712
    {
713
        $this->server_caps = null;
714
        $this->helo_rply = null;
715
        if (is_resource($this->smtp_conn)) {
716
            //Close the connection and cleanup
717
            fclose($this->smtp_conn);
718
            $this->smtp_conn = null; //Makes for cleaner serialization
719
            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
720
        }
721
    }
722
723
    /**
724
     * Send an SMTP DATA command.
725
     * Issues a data command and sends the msg_data to the server,
726
     * finalizing the mail transaction. $msg_data is the message
727
     * that is to be sent with the headers. Each header needs to be
728
     * on a single line followed by a <CRLF> with the message headers
729
     * and the message body being separated by an additional <CRLF>.
730
     * Implements RFC 821: DATA <CRLF>.
731
     *
732
     * @param string $msg_data Message data to send
733
     *
734
     * @return bool
735
     */
736
    public function data($msg_data)
737
    {
738
        //This will use the standard timelimit
739
        if (!$this->sendCommand('DATA', 'DATA', 354)) {
740
            return false;
741
        }
742
743
        /* The server is ready to accept data!
744
         * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
745
         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
746
         * smaller lines to fit within the limit.
747
         * We will also look for lines that start with a '.' and prepend an additional '.'.
748
         * NOTE: this does not count towards line-length limit.
749
         */
750
751
        //Normalize line breaks before exploding
752
        $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
753
754
        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
755
         * of the first line (':' separated) does not contain a space then it _should_ be a header, and we will
756
         * process all lines before a blank line as headers.
757
         */
758
759
        $field = substr($lines[0], 0, strpos($lines[0], ':'));
760
        $in_headers = false;
761
        if (!empty($field) && strpos($field, ' ') === false) {
762
            $in_headers = true;
763
        }
764
765
        foreach ($lines as $line) {
766
            $lines_out = [];
767
            if ($in_headers && $line === '') {
768
                $in_headers = false;
769
            }
770
            //Break this line up into several smaller lines if it's too long
771
            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
772
            while (isset($line[self::MAX_LINE_LENGTH])) {
773
                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
774
                //so as to avoid breaking in the middle of a word
775
                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
776
                //Deliberately matches both false and 0
777
                if (!$pos) {
778
                    //No nice break found, add a hard break
779
                    $pos = self::MAX_LINE_LENGTH - 1;
780
                    $lines_out[] = substr($line, 0, $pos);
781
                    $line = substr($line, $pos);
782
                } else {
783
                    //Break at the found point
784
                    $lines_out[] = substr($line, 0, $pos);
785
                    //Move along by the amount we dealt with
786
                    $line = substr($line, $pos + 1);
787
                }
788
                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
789
                if ($in_headers) {
790
                    $line = "\t" . $line;
791
                }
792
            }
793
            $lines_out[] = $line;
794
795
            //Send the lines to the server
796
            foreach ($lines_out as $line_out) {
797
                //Dot-stuffing as per RFC5321 section 4.5.2
798
                //https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
799
                if (!empty($line_out) && $line_out[0] === '.') {
800
                    $line_out = '.' . $line_out;
801
                }
802
                $this->client_send($line_out . static::LE, 'DATA');
803
            }
804
        }
805
806
        //Message data has been sent, complete the command
807
        //Increase timelimit for end of DATA command
808
        $savetimelimit = $this->Timelimit;
809
        $this->Timelimit *= 2;
810
        $result = $this->sendCommand('DATA END', '.', 250);
811
        $this->recordLastTransactionID();
812
        //Restore timelimit
813
        $this->Timelimit = $savetimelimit;
814
815
        return $result;
816
    }
817
818
    /**
819
     * Send an SMTP HELO or EHLO command.
820
     * Used to identify the sending server to the receiving server.
821
     * This makes sure that client and server are in a known state.
822
     * Implements RFC 821: HELO <SP> <domain> <CRLF>
823
     * and RFC 2821 EHLO.
824
     *
825
     * @param string $host The host name or IP to connect to
826
     *
827
     * @return bool
828
     */
829
    public function hello($host = '')
830
    {
831
        //Try extended hello first (RFC 2821)
832
        if ($this->sendHello('EHLO', $host)) {
833
            return true;
834
        }
835
836
        //Some servers shut down the SMTP service here (RFC 5321)
837
        if (substr($this->helo_rply, 0, 3) == '421') {
0 ignored issues
show
Bug introduced by
It seems like $this->helo_rply can also be of type null; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

837
        if (substr(/** @scrutinizer ignore-type */ $this->helo_rply, 0, 3) == '421') {
Loading history...
838
            return false;
839
        }
840
841
        return $this->sendHello('HELO', $host);
842
    }
843
844
    /**
845
     * Send an SMTP HELO or EHLO command.
846
     * Low-level implementation used by hello().
847
     *
848
     * @param string $hello The HELO string
849
     * @param string $host  The hostname to say we are
850
     *
851
     * @return bool
852
     *
853
     * @see hello()
854
     */
855
    protected function sendHello($hello, $host)
856
    {
857
        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
858
        $this->helo_rply = $this->last_reply;
859
        if ($noerror) {
860
            $this->parseHelloFields($hello);
861
        } else {
862
            $this->server_caps = null;
863
        }
864
865
        return $noerror;
866
    }
867
868
    /**
869
     * Parse a reply to HELO/EHLO command to discover server extensions.
870
     * In case of HELO, the only parameter that can be discovered is a server name.
871
     *
872
     * @param string $type `HELO` or `EHLO`
873
     */
874
    protected function parseHelloFields($type)
875
    {
876
        $this->server_caps = [];
877
        $lines = explode("\n", $this->helo_rply);
0 ignored issues
show
Bug introduced by
It seems like $this->helo_rply can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

877
        $lines = explode("\n", /** @scrutinizer ignore-type */ $this->helo_rply);
Loading history...
878
879
        foreach ($lines as $n => $s) {
880
            //First 4 chars contain response code followed by - or space
881
            $s = trim(substr($s, 4));
882
            if (empty($s)) {
883
                continue;
884
            }
885
            $fields = explode(' ', $s);
886
            if (!empty($fields)) {
887
                if (!$n) {
888
                    $name = $type;
889
                    $fields = $fields[0];
890
                } else {
891
                    $name = array_shift($fields);
892
                    switch ($name) {
893
                        case 'SIZE':
894
                            $fields = ($fields ? $fields[0] : 0);
895
                            break;
896
                        case 'AUTH':
897
                            if (!is_array($fields)) {
898
                                $fields = [];
899
                            }
900
                            break;
901
                        default:
902
                            $fields = true;
903
                    }
904
                }
905
                $this->server_caps[$name] = $fields;
906
            }
907
        }
908
    }
909
910
    /**
911
     * Send an SMTP MAIL command.
912
     * Starts a mail transaction from the email address specified in
913
     * $from. Returns true if successful or false otherwise. If True
914
     * the mail transaction is started and then one or more recipient
915
     * commands may be called followed by a data command.
916
     * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
917
     *
918
     * @param string $from Source address of this message
919
     *
920
     * @return bool
921
     */
922
    public function mail($from)
923
    {
924
        $useVerp = ($this->do_verp ? ' XVERP' : '');
925
926
        return $this->sendCommand(
927
            'MAIL FROM',
928
            'MAIL FROM:<' . $from . '>' . $useVerp,
929
            250
930
        );
931
    }
932
933
    /**
934
     * Send an SMTP QUIT command.
935
     * Closes the socket if there is no error or the $close_on_error argument is true.
936
     * Implements from RFC 821: QUIT <CRLF>.
937
     *
938
     * @param bool $close_on_error Should the connection close if an error occurs?
939
     *
940
     * @return bool
941
     */
942
    public function quit($close_on_error = true)
943
    {
944
        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
945
        $err = $this->error; //Save any error
946
        if ($noerror || $close_on_error) {
947
            $this->close();
948
            $this->error = $err; //Restore any error from the quit command
949
        }
950
951
        return $noerror;
952
    }
953
954
    /**
955
     * Send an SMTP RCPT command.
956
     * Sets the TO argument to $toaddr.
957
     * Returns true if the recipient was accepted false if it was rejected.
958
     * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
959
     *
960
     * @param string $address The address the message is being sent to
961
     * @param string $dsn     Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
962
     *                        or DELAY. If you specify NEVER all other notifications are ignored.
963
     *
964
     * @return bool
965
     */
966
    public function recipient($address, $dsn = '')
967
    {
968
        if (empty($dsn)) {
969
            $rcpt = 'RCPT TO:<' . $address . '>';
970
        } else {
971
            $dsn = strtoupper($dsn);
972
            $notify = [];
973
974
            if (strpos($dsn, 'NEVER') !== false) {
975
                $notify[] = 'NEVER';
976
            } else {
977
                foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
978
                    if (strpos($dsn, $value) !== false) {
979
                        $notify[] = $value;
980
                    }
981
                }
982
            }
983
984
            $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
985
        }
986
987
        return $this->sendCommand(
988
            'RCPT TO',
989
            $rcpt,
990
            [250, 251]
991
        );
992
    }
993
994
    /**
995
     * Send SMTP XCLIENT command to server and check its return code.
996
     *
997
     * @return bool True on success
998
     */
999
    public function xclient(array $vars)
1000
    {
1001
        $xclient_options = "";
1002
        foreach ($vars as $key => $value) {
1003
            if (in_array($key, SMTP::$xclient_allowed_attributes)) {
1004
                $xclient_options .= " {$key}={$value}";
1005
            }
1006
        }
1007
        if (!$xclient_options) {
1008
            return true;
1009
        }
1010
        return $this->sendCommand('XCLIENT', 'XCLIENT' . $xclient_options, 250);
1011
    }
1012
1013
    /**
1014
     * Send an SMTP RSET command.
1015
     * Abort any transaction that is currently in progress.
1016
     * Implements RFC 821: RSET <CRLF>.
1017
     *
1018
     * @return bool True on success
1019
     */
1020
    public function reset()
1021
    {
1022
        return $this->sendCommand('RSET', 'RSET', 250);
1023
    }
1024
1025
    /**
1026
     * Send a command to an SMTP server and check its return code.
1027
     *
1028
     * @param string    $command       The command name - not sent to the server
1029
     * @param string    $commandstring The actual command to send
1030
     * @param int|array $expect        One or more expected integer success codes
1031
     *
1032
     * @return bool True on success
1033
     */
1034
    protected function sendCommand($command, $commandstring, $expect)
1035
    {
1036
        if (!$this->connected()) {
1037
            $this->setError("Called $command without being connected");
1038
1039
            return false;
1040
        }
1041
        //Reject line breaks in all commands
1042
        if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
1043
            $this->setError("Command '$command' contained line breaks");
1044
1045
            return false;
1046
        }
1047
        $this->client_send($commandstring . static::LE, $command);
1048
1049
        $this->last_reply = $this->get_lines();
1050
        //Fetch SMTP code and possible error code explanation
1051
        $matches = [];
1052
        if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
1053
            $code = (int) $matches[1];
1054
            $code_ex = (count($matches) > 2 ? $matches[2] : null);
1055
            //Cut off error code from each response line
1056
            $detail = preg_replace(
1057
                "/{$code}[ -]" .
1058
                ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
1059
                '',
1060
                $this->last_reply
1061
            );
1062
        } else {
1063
            //Fall back to simple parsing if regex fails
1064
            $code = (int) substr($this->last_reply, 0, 3);
1065
            $code_ex = null;
1066
            $detail = substr($this->last_reply, 4);
1067
        }
1068
1069
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
1070
1071
        if (!in_array($code, (array) $expect, true)) {
1072
            $this->setError(
1073
                "$command command failed",
1074
                $detail,
1075
                $code,
1076
                $code_ex
1077
            );
1078
            $this->edebug(
1079
                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
1080
                self::DEBUG_CLIENT
1081
            );
1082
1083
            return false;
1084
        }
1085
1086
        //Don't clear the error store when using keepalive
1087
        if ($command !== 'RSET') {
1088
            $this->setError('');
1089
        }
1090
1091
        return true;
1092
    }
1093
1094
    /**
1095
     * Send an SMTP SAML command.
1096
     * Starts a mail transaction from the email address specified in $from.
1097
     * Returns true if successful or false otherwise. If True
1098
     * the mail transaction is started and then one or more recipient
1099
     * commands may be called followed by a data command. This command
1100
     * will send the message to the users terminal if they are logged
1101
     * in and send them an email.
1102
     * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
1103
     *
1104
     * @param string $from The address the message is from
1105
     *
1106
     * @return bool
1107
     */
1108
    public function sendAndMail($from)
1109
    {
1110
        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
1111
    }
1112
1113
    /**
1114
     * Send an SMTP VRFY command.
1115
     *
1116
     * @param string $name The name to verify
1117
     *
1118
     * @return bool
1119
     */
1120
    public function verify($name)
1121
    {
1122
        return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
1123
    }
1124
1125
    /**
1126
     * Send an SMTP NOOP command.
1127
     * Used to keep keep-alives alive, doesn't actually do anything.
1128
     *
1129
     * @return bool
1130
     */
1131
    public function noop()
1132
    {
1133
        return $this->sendCommand('NOOP', 'NOOP', 250);
1134
    }
1135
1136
    /**
1137
     * Send an SMTP TURN command.
1138
     * This is an optional command for SMTP that this class does not support.
1139
     * This method is here to make the RFC821 Definition complete for this class
1140
     * and _may_ be implemented in future.
1141
     * Implements from RFC 821: TURN <CRLF>.
1142
     *
1143
     * @return bool
1144
     */
1145
    public function turn()
1146
    {
1147
        $this->setError('The SMTP TURN command is not implemented');
1148
        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
1149
1150
        return false;
1151
    }
1152
1153
    /**
1154
     * Send raw data to the server.
1155
     *
1156
     * @param string $data    The data to send
1157
     * @param string $command Optionally, the command this is part of, used only for controlling debug output
1158
     *
1159
     * @return int|bool The number of bytes sent to the server or false on error
1160
     */
1161
    public function client_send($data, $command = '')
1162
    {
1163
        //If SMTP transcripts are left enabled, or debug output is posted online
1164
        //it can leak credentials, so hide credentials in all but lowest level
1165
        if (
1166
            self::DEBUG_LOWLEVEL > $this->do_debug &&
1167
            in_array($command, ['User & Password', 'Username', 'Password'], true)
1168
        ) {
1169
            $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
1170
        } else {
1171
            $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
1172
        }
1173
        set_error_handler(function () {
1174
            call_user_func_array([$this, 'errorHandler'], func_get_args());
1175
        });
1176
        $result = fwrite($this->smtp_conn, $data);
1177
        restore_error_handler();
1178
1179
        return $result;
1180
    }
1181
1182
    /**
1183
     * Get the latest error.
1184
     *
1185
     * @return array
1186
     */
1187
    public function getError()
1188
    {
1189
        return $this->error;
1190
    }
1191
1192
    /**
1193
     * Get SMTP extensions available on the server.
1194
     *
1195
     * @return array|null
1196
     */
1197
    public function getServerExtList()
1198
    {
1199
        return $this->server_caps;
1200
    }
1201
1202
    /**
1203
     * Get metadata about the SMTP server from its HELO/EHLO response.
1204
     * The method works in three ways, dependent on argument value and current state:
1205
     *   1. HELO/EHLO has not been sent - returns null and populates $this->error.
1206
     *   2. HELO has been sent -
1207
     *     $name == 'HELO': returns server name
1208
     *     $name == 'EHLO': returns boolean false
1209
     *     $name == any other string: returns null and populates $this->error
1210
     *   3. EHLO has been sent -
1211
     *     $name == 'HELO'|'EHLO': returns the server name
1212
     *     $name == any other string: if extension $name exists, returns True
1213
     *       or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
1214
     *
1215
     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
1216
     *
1217
     * @return string|bool|null
1218
     */
1219
    public function getServerExt($name)
1220
    {
1221
        if (!$this->server_caps) {
1222
            $this->setError('No HELO/EHLO was sent');
1223
1224
            return null;
1225
        }
1226
1227
        if (!array_key_exists($name, $this->server_caps)) {
1228
            if ('HELO' === $name) {
1229
                return $this->server_caps['EHLO'];
1230
            }
1231
            if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
1232
                return false;
1233
            }
1234
            $this->setError('HELO handshake was used; No information about server extensions available');
1235
1236
            return null;
1237
        }
1238
1239
        return $this->server_caps[$name];
1240
    }
1241
1242
    /**
1243
     * Get the last reply from the server.
1244
     *
1245
     * @return string
1246
     */
1247
    public function getLastReply()
1248
    {
1249
        return $this->last_reply;
1250
    }
1251
1252
    /**
1253
     * Read the SMTP server's response.
1254
     * Either before eof or socket timeout occurs on the operation.
1255
     * With SMTP we can tell if we have more lines to read if the
1256
     * 4th character is '-' symbol. If it is a space then we don't
1257
     * need to read anything else.
1258
     *
1259
     * @return string
1260
     */
1261
    protected function get_lines()
1262
    {
1263
        //If the connection is bad, give up straight away
1264
        if (!is_resource($this->smtp_conn)) {
1265
            return '';
1266
        }
1267
        $data = '';
1268
        $endtime = 0;
1269
        stream_set_timeout($this->smtp_conn, $this->Timeout);
1270
        if ($this->Timelimit > 0) {
1271
            $endtime = time() + $this->Timelimit;
1272
        }
1273
        $selR = [$this->smtp_conn];
1274
        $selW = null;
1275
        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1276
            //Must pass vars in here as params are by reference
1277
            //solution for signals inspired by https://github.com/symfony/symfony/pull/6540
1278
            set_error_handler(function () {
1279
                call_user_func_array([$this, 'errorHandler'], func_get_args());
1280
            });
1281
            $n = stream_select($selR, $selW, $selW, $this->Timelimit);
1282
            restore_error_handler();
1283
1284
            if ($n === false) {
1285
                $message = $this->getError()['detail'];
1286
1287
                $this->edebug(
1288
                    'SMTP -> get_lines(): select failed (' . $message . ')',
1289
                    self::DEBUG_LOWLEVEL
1290
                );
1291
1292
                //stream_select returns false when the `select` system call is interrupted
1293
                //by an incoming signal, try the select again
1294
                if (stripos($message, 'interrupted system call') !== false) {
1295
                    $this->edebug(
1296
                        'SMTP -> get_lines(): retrying stream_select',
1297
                        self::DEBUG_LOWLEVEL
1298
                    );
1299
                    $this->setError('');
1300
                    continue;
1301
                }
1302
1303
                break;
1304
            }
1305
1306
            if (!$n) {
1307
                $this->edebug(
1308
                    'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
1309
                    self::DEBUG_LOWLEVEL
1310
                );
1311
                break;
1312
            }
1313
1314
            //Deliberate noise suppression - errors are handled afterwards
1315
            $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
1316
            $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
0 ignored issues
show
Bug introduced by
It seems like $str can also be of type false; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1316
            $this->edebug('SMTP INBOUND: "' . trim(/** @scrutinizer ignore-type */ $str) . '"', self::DEBUG_LOWLEVEL);
Loading history...
1317
            $data .= $str;
1318
            //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
1319
            //or 4th character is a space or a line break char, we are done reading, break the loop.
1320
            //String array access is a significant micro-optimisation over strlen
1321
            if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
1322
                break;
1323
            }
1324
            //Timed-out? Log and break
1325
            $info = stream_get_meta_data($this->smtp_conn);
1326
            if ($info['timed_out']) {
1327
                $this->edebug(
1328
                    'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
1329
                    self::DEBUG_LOWLEVEL
1330
                );
1331
                break;
1332
            }
1333
            //Now check if reads took too long
1334
            if ($endtime && time() > $endtime) {
1335
                $this->edebug(
1336
                    'SMTP -> get_lines(): timelimit reached (' .
1337
                    $this->Timelimit . ' sec)',
1338
                    self::DEBUG_LOWLEVEL
1339
                );
1340
                break;
1341
            }
1342
        }
1343
1344
        return $data;
1345
    }
1346
1347
    /**
1348
     * Enable or disable VERP address generation.
1349
     *
1350
     * @param bool $enabled
1351
     */
1352
    public function setVerp($enabled = false)
1353
    {
1354
        $this->do_verp = $enabled;
1355
    }
1356
1357
    /**
1358
     * Get VERP address generation mode.
1359
     *
1360
     * @return bool
1361
     */
1362
    public function getVerp()
1363
    {
1364
        return $this->do_verp;
1365
    }
1366
1367
    /**
1368
     * Set error messages and codes.
1369
     *
1370
     * @param string $message      The error message
1371
     * @param string $detail       Further detail on the error
1372
     * @param string $smtp_code    An associated SMTP error code
1373
     * @param string $smtp_code_ex Extended SMTP code
1374
     */
1375
    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1376
    {
1377
        $this->error = [
1378
            'error' => $message,
1379
            'detail' => $detail,
1380
            'smtp_code' => $smtp_code,
1381
            'smtp_code_ex' => $smtp_code_ex,
1382
        ];
1383
    }
1384
1385
    /**
1386
     * Set debug output method.
1387
     *
1388
     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
1389
     */
1390
    public function setDebugOutput($method = 'echo')
1391
    {
1392
        $this->Debugoutput = $method;
1393
    }
1394
1395
    /**
1396
     * Get debug output method.
1397
     *
1398
     * @return string
1399
     */
1400
    public function getDebugOutput()
1401
    {
1402
        return $this->Debugoutput;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->Debugoutput also could return the type callable which is incompatible with the documented return type string.
Loading history...
1403
    }
1404
1405
    /**
1406
     * Set debug output level.
1407
     *
1408
     * @param int $level
1409
     */
1410
    public function setDebugLevel($level = 0)
1411
    {
1412
        $this->do_debug = $level;
1413
    }
1414
1415
    /**
1416
     * Get debug output level.
1417
     *
1418
     * @return int
1419
     */
1420
    public function getDebugLevel()
1421
    {
1422
        return $this->do_debug;
1423
    }
1424
1425
    /**
1426
     * Set SMTP timeout.
1427
     *
1428
     * @param int $timeout The timeout duration in seconds
1429
     */
1430
    public function setTimeout($timeout = 0)
1431
    {
1432
        $this->Timeout = $timeout;
1433
    }
1434
1435
    /**
1436
     * Get SMTP timeout.
1437
     *
1438
     * @return int
1439
     */
1440
    public function getTimeout()
1441
    {
1442
        return $this->Timeout;
1443
    }
1444
1445
    /**
1446
     * Reports an error number and string.
1447
     *
1448
     * @param int    $errno   The error number returned by PHP
1449
     * @param string $errmsg  The error message returned by PHP
1450
     * @param string $errfile The file the error occurred in
1451
     * @param int    $errline The line number the error occurred on
1452
     */
1453
    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1454
    {
1455
        $notice = 'Connection failed.';
1456
        $this->setError(
1457
            $notice,
1458
            $errmsg,
1459
            (string) $errno
1460
        );
1461
        $this->edebug(
1462
            "$notice Error #$errno: $errmsg [$errfile line $errline]",
1463
            self::DEBUG_CONNECTION
1464
        );
1465
    }
1466
1467
    /**
1468
     * Extract and return the ID of the last SMTP transaction based on
1469
     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1470
     * Relies on the host providing the ID in response to a DATA command.
1471
     * If no reply has been received yet, it will return null.
1472
     * If no pattern was matched, it will return false.
1473
     *
1474
     * @return bool|string|null
1475
     */
1476
    protected function recordLastTransactionID()
1477
    {
1478
        $reply = $this->getLastReply();
1479
1480
        if (empty($reply)) {
1481
            $this->last_smtp_transaction_id = null;
1482
        } else {
1483
            $this->last_smtp_transaction_id = false;
1484
            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1485
                $matches = [];
1486
                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1487
                    $this->last_smtp_transaction_id = trim($matches[1]);
1488
                    break;
1489
                }
1490
            }
1491
        }
1492
1493
        return $this->last_smtp_transaction_id;
1494
    }
1495
1496
    /**
1497
     * Get the queue/transaction ID of the last SMTP transaction
1498
     * If no reply has been received yet, it will return null.
1499
     * If no pattern was matched, it will return false.
1500
     *
1501
     * @return bool|string|null
1502
     *
1503
     * @see recordLastTransactionID()
1504
     */
1505
    public function getLastTransactionID()
1506
    {
1507
        return $this->last_smtp_transaction_id;
1508
    }
1509
}
1510