SMTP::getVerp()   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 0
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.10.0';
39
40
    /**
41
     * SMTP line break constant.
42
     *
43
     * @var string
44
     */
45
    const LE = "\r\n";
46
47
    /**
48
     * The SMTP port to use if one is not specified.
49
     *
50
     * @var int
51
     */
52
    const DEFAULT_PORT = 25;
53
54
    /**
55
     * The 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
     * Whether to use SMTPUTF8.
164
     *
165
     * @see https://www.rfc-editor.org/rfc/rfc6531
166
     *
167
     * @var bool
168
     */
169
    public $do_smtputf8 = false;
170
171
    /**
172
     * The timeout value for connection, in seconds.
173
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
174
     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
175
     *
176
     * @see https://www.rfc-editor.org/rfc/rfc2821#section-4.5.3.2
177
     *
178
     * @var int
179
     */
180
    public $Timeout = 300;
181
182
    /**
183
     * How long to wait for commands to complete, in seconds.
184
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
185
     *
186
     * @var int
187
     */
188
    public $Timelimit = 300;
189
190
    /**
191
     * Patterns to extract an SMTP transaction id from reply to a DATA command.
192
     * The first capture group in each regex will be used as the ID.
193
     * MS ESMTP returns the message ID, which may not be correct for internal tracking.
194
     *
195
     * @var string[]
196
     */
197
    protected $smtp_transaction_id_patterns = [
198
        'exim' => '/[\d]{3} OK id=(.*)/',
199
        'sendmail' => '/[\d]{3} 2\.0\.0 (.*) Message/',
200
        'postfix' => '/[\d]{3} 2\.0\.0 Ok: queued as (.*)/',
201
        'Microsoft_ESMTP' => '/[0-9]{3} 2\.[\d]\.0 (.*)@(?:.*) Queued mail for delivery/',
202
        'Amazon_SES' => '/[\d]{3} Ok (.*)/',
203
        'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
204
        'CampaignMonitor' => '/[\d]{3} 2\.0\.0 OK:([a-zA-Z\d]{48})/',
205
        'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
206
        'ZoneMTA' => '/[\d]{3} Message queued as (.*)/',
207
        'Mailjet' => '/[\d]{3} OK queued as (.*)/',
208
    ];
209
210
    /**
211
     * Allowed SMTP XCLIENT attributes.
212
     * Must be allowed by the SMTP server. EHLO response is not checked.
213
     *
214
     * @see https://www.postfix.org/XCLIENT_README.html
215
     *
216
     * @var array
217
     */
218
    public static $xclient_allowed_attributes = [
219
        'NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'LOGIN', 'DESTADDR', 'DESTPORT'
220
    ];
221
222
    /**
223
     * The last transaction ID issued in response to a DATA command,
224
     * if one was detected.
225
     *
226
     * @var string|bool|null
227
     */
228
    protected $last_smtp_transaction_id;
229
230
    /**
231
     * The socket for the server connection.
232
     *
233
     * @var ?resource
234
     */
235
    protected $smtp_conn;
236
237
    /**
238
     * Error information, if any, for the last SMTP command.
239
     *
240
     * @var array
241
     */
242
    protected $error = [
243
        'error' => '',
244
        'detail' => '',
245
        'smtp_code' => '',
246
        'smtp_code_ex' => '',
247
    ];
248
249
    /**
250
     * The reply the server sent to us for HELO.
251
     * If null, no HELO string has yet been received.
252
     *
253
     * @var string|null
254
     */
255
    protected $helo_rply;
256
257
    /**
258
     * The set of SMTP extensions sent in reply to EHLO command.
259
     * Indexes of the array are extension names.
260
     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
261
     * represents the server name. In case of HELO it is the only element of the array.
262
     * Other values can be boolean TRUE or an array containing extension options.
263
     * If null, no HELO/EHLO string has yet been received.
264
     *
265
     * @var array|null
266
     */
267
    protected $server_caps;
268
269
    /**
270
     * The most recent reply received from the server.
271
     *
272
     * @var string
273
     */
274
    protected $last_reply = '';
275
276
    /**
277
     * Output debugging info via a user-selected method.
278
     *
279
     * @param string $str   Debug string to output
280
     * @param int    $level The debug level of this message; see DEBUG_* constants
281
     *
282
     * @see SMTP::$Debugoutput
283
     * @see SMTP::$do_debug
284
     */
285
    protected function edebug($str, $level = 0)
286
    {
287
        if ($level > $this->do_debug) {
288
            return;
289
        }
290
        //Is this a PSR-3 logger?
291
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
292
            //Remove trailing line breaks potentially added by calls to SMTP::client_send()
293
            $this->Debugoutput->debug(rtrim($str, "\r\n"));
294
295
            return;
296
        }
297
        //Avoid clash with built-in function names
298
        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
299
            call_user_func($this->Debugoutput, $str, $level);
300
301
            return;
302
        }
303
        switch ($this->Debugoutput) {
304
            case 'error_log':
305
                //Don't output, just log
306
                /** @noinspection ForgottenDebugOutputInspection */
307
                error_log($str);
308
                break;
309
            case 'html':
310
                //Cleans up output a bit for a better looking, HTML-safe output
311
                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
312
                    preg_replace('/[\r\n]+/', '', $str),
313
                    ENT_QUOTES,
314
                    'UTF-8'
315
                ), "<br>\n";
316
                break;
317
            case 'echo':
318
            default:
319
                //Normalize line breaks
320
                $str = preg_replace('/\r\n|\r/m', "\n", $str);
321
                echo gmdate('Y-m-d H:i:s'),
322
                "\t",
323
                    //Trim trailing space
324
                trim(
325
                    //Indent for readability, except for trailing break
326
                    str_replace(
327
                        "\n",
328
                        "\n                   \t                  ",
329
                        trim($str)
330
                    )
331
                ),
332
                "\n";
333
        }
334
    }
335
336
    /**
337
     * Connect to an SMTP server.
338
     *
339
     * @param string $host    SMTP server IP or host name
340
     * @param int    $port    The port number to connect to
341
     * @param int    $timeout How long to wait for the connection to open
342
     * @param array  $options An array of options for stream_context_create()
343
     *
344
     * @return bool
345
     */
346
    public function connect($host, $port = null, $timeout = 30, $options = [])
347
    {
348
        //Clear errors to avoid confusion
349
        $this->setError('');
350
        //Make sure we are __not__ connected
351
        if ($this->connected()) {
352
            //Already connected, generate error
353
            $this->setError('Already connected to a server');
354
355
            return false;
356
        }
357
        if (empty($port)) {
358
            $port = self::DEFAULT_PORT;
359
        }
360
        //Connect to the SMTP server
361
        $this->edebug(
362
            "Connection: opening to $host:$port, timeout=$timeout, options=" .
363
            (count($options) > 0 ? var_export($options, true) : 'array()'),
364
            self::DEBUG_CONNECTION
365
        );
366
367
        $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...
368
369
        if ($this->smtp_conn === false) {
370
            //Error info already set inside `getSMTPConnection()`
371
            return false;
372
        }
373
374
        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
375
376
        //Get any announcement
377
        $this->last_reply = $this->get_lines();
378
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
379
        $responseCode = (int)substr($this->last_reply, 0, 3);
380
        if ($responseCode === 220) {
381
            return true;
382
        }
383
        //Anything other than a 220 response means something went wrong
384
        //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
385
        //https://www.rfc-editor.org/rfc/rfc5321#section-3.1
386
        if ($responseCode === 554) {
387
            $this->quit();
388
        }
389
        //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
390
        $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
391
        $this->close();
392
        return false;
393
    }
394
395
    /**
396
     * Create connection to the SMTP server.
397
     *
398
     * @param string $host    SMTP server IP or host name
399
     * @param int    $port    The port number to connect to
400
     * @param int    $timeout How long to wait for the connection to open
401
     * @param array  $options An array of options for stream_context_create()
402
     *
403
     * @return false|resource
404
     */
405
    protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
406
    {
407
        static $streamok;
408
        //This is enabled by default since 5.0.0 but some providers disable it
409
        //Check this once and cache the result
410
        if (null === $streamok) {
411
            $streamok = function_exists('stream_socket_client');
412
        }
413
414
        $errno = 0;
415
        $errstr = '';
416
        if ($streamok) {
417
            $socket_context = stream_context_create($options);
418
            set_error_handler(function () {
419
                call_user_func_array([$this, 'errorHandler'], func_get_args());
420
            });
421
            $connection = stream_socket_client(
422
                $host . ':' . $port,
423
                $errno,
424
                $errstr,
425
                $timeout,
426
                STREAM_CLIENT_CONNECT,
427
                $socket_context
428
            );
429
        } else {
430
            //Fall back to fsockopen which should work in more places, but is missing some features
431
            $this->edebug(
432
                'Connection: stream_socket_client not available, falling back to fsockopen',
433
                self::DEBUG_CONNECTION
434
            );
435
            set_error_handler(function () {
436
                call_user_func_array([$this, 'errorHandler'], func_get_args());
437
            });
438
            $connection = fsockopen(
439
                $host,
440
                $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

440
                /** @scrutinizer ignore-type */ $port,
Loading history...
441
                $errno,
442
                $errstr,
443
                $timeout
444
            );
445
        }
446
        restore_error_handler();
447
448
        //Verify we connected properly
449
        if (!is_resource($connection)) {
450
            $this->setError(
451
                'Failed to connect to server',
452
                '',
453
                (string) $errno,
454
                $errstr
455
            );
456
            $this->edebug(
457
                'SMTP ERROR: ' . $this->error['error']
458
                . ": $errstr ($errno)",
459
                self::DEBUG_CLIENT
460
            );
461
462
            return false;
463
        }
464
465
        //SMTP server can take longer to respond, give longer timeout for first read
466
        //Windows does not have support for this timeout function
467
        if (strpos(PHP_OS, 'WIN') !== 0) {
468
            $max = (int)ini_get('max_execution_time');
469
            //Don't bother if unlimited, or if set_time_limit is disabled
470
            if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
471
                @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

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

846
        if (substr(/** @scrutinizer ignore-type */ $this->helo_rply, 0, 3) == '421') {
Loading history...
847
            return false;
848
        }
849
850
        return $this->sendHello('HELO', $host);
851
    }
852
853
    /**
854
     * Send an SMTP HELO or EHLO command.
855
     * Low-level implementation used by hello().
856
     *
857
     * @param string $hello The HELO string
858
     * @param string $host  The hostname to say we are
859
     *
860
     * @return bool
861
     *
862
     * @see hello()
863
     */
864
    protected function sendHello($hello, $host)
865
    {
866
        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
867
        $this->helo_rply = $this->last_reply;
868
        if ($noerror) {
869
            $this->parseHelloFields($hello);
870
        } else {
871
            $this->server_caps = null;
872
        }
873
874
        return $noerror;
875
    }
876
877
    /**
878
     * Parse a reply to HELO/EHLO command to discover server extensions.
879
     * In case of HELO, the only parameter that can be discovered is a server name.
880
     *
881
     * @param string $type `HELO` or `EHLO`
882
     */
883
    protected function parseHelloFields($type)
884
    {
885
        $this->server_caps = [];
886
        $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

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

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