SMTP   F
last analyzed

Complexity

Total Complexity 151

Size/Duplication

Total Lines 1423
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 480
c 1
b 0
f 0
dl 0
loc 1423
rs 2
wmc 151

39 Methods

Rating   Name   Duplication   Size   Complexity  
A client_send() 0 17 3
C get_lines() 0 82 15
F authenticate() 0 122 25
A setDebugLevel() 0 3 1
A recordLastTransactionID() 0 18 4
C data() 0 80 13
A setDebugOutput() 0 3 1
B getSMTPConnection() 0 69 8
A startTLS() 0 26 3
A getDebugLevel() 0 3 1
A getVerp() 0 3 1
A verify() 0 3 1
A getLastReply() 0 3 1
B edebug() 0 46 8
A mail() 0 8 2
A errorHandler() 0 11 1
A turn() 0 6 1
A hello() 0 13 3
A getError() 0 3 1
A quit() 0 10 3
B sendCommand() 0 55 8
A setTimeout() 0 3 1
B connect() 0 47 7
A getDebugOutput() 0 3 1
A recipient() 0 25 5
B parseHelloFields() 0 32 9
A sendHello() 0 11 2
A connected() 0 19 3
A getTimeout() 0 3 1
A setVerp() 0 3 1
A close() 0 10 2
A hmac() 0 25 3
A getServerExtList() 0 3 1
A noop() 0 3 1
A reset() 0 3 1
A setError() 0 7 1
A sendAndMail() 0 3 1
A getLastTransactionID() 0 3 1
A getServerExt() 0 21 6

How to fix   Complexity   

Complex Class

Complex classes like SMTP often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

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

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

1
<?php
2
3
/**
4
 * PHPMailer RFC821 SMTP email transport class.
5
 * PHP Version 5.5.
6
 *
7
 * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
8
 *
9
 * @author    Marcus Bointon (Synchro/coolbru) <[email protected]>
10
 * @author    Jim Jagielski (jimjag) <[email protected]>
11
 * @author    Andy Prevost (codeworxtech) <[email protected]>
12
 * @author    Brent R. Matzelle (original founder)
13
 * @copyright 2012 - 2020 Marcus Bointon
14
 * @copyright 2010 - 2012 Jim Jagielski
15
 * @copyright 2004 - 2009 Andy Prevost
16
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
17
 * @note      This program is distributed in the hope that it will be useful - WITHOUT
18
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
 * FITNESS FOR A PARTICULAR PURPOSE.
20
 */
21
22
namespace PHPMailer\PHPMailer;
23
24
/**
25
 * PHPMailer RFC821 SMTP email transport class.
26
 * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
27
 *
28
 * @author Chris Ryan
29
 * @author Marcus Bointon <[email protected]>
30
 */
31
class SMTP
32
{
33
    /**
34
     * The PHPMailer SMTP version number.
35
     *
36
     * @var string
37
     */
38
    const VERSION = '6.4.0';
39
40
    /**
41
     * SMTP line break constant.
42
     *
43
     * @var string
44
     */
45
    const LE = "\r\n";
46
47
    /**
48
     * The SMTP port to use if one is not specified.
49
     *
50
     * @var int
51
     */
52
    const DEFAULT_PORT = 25;
53
54
    /**
55
     * The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
56
     * *excluding* a trailing CRLF break.
57
     *
58
     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6
59
     *
60
     * @var int
61
     */
62
    const MAX_LINE_LENGTH = 998;
63
64
    /**
65
     * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
66
     * *including* a trailing CRLF line break.
67
     *
68
     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5
69
     *
70
     * @var int
71
     */
72
    const MAX_REPLY_LENGTH = 512;
73
74
    /**
75
     * Debug level for no output.
76
     *
77
     * @var int
78
     */
79
    const DEBUG_OFF = 0;
80
81
    /**
82
     * Debug level to show client -> server messages.
83
     *
84
     * @var int
85
     */
86
    const DEBUG_CLIENT = 1;
87
88
    /**
89
     * Debug level to show client -> server and server -> client messages.
90
     *
91
     * @var int
92
     */
93
    const DEBUG_SERVER = 2;
94
95
    /**
96
     * Debug level to show connection status, client -> server and server -> client messages.
97
     *
98
     * @var int
99
     */
100
    const DEBUG_CONNECTION = 3;
101
102
    /**
103
     * Debug level to show all messages.
104
     *
105
     * @var int
106
     */
107
    const DEBUG_LOWLEVEL = 4;
108
109
    /**
110
     * Debug output level.
111
     * Options:
112
     * * self::DEBUG_OFF (`0`) No debug output, default
113
     * * self::DEBUG_CLIENT (`1`) Client commands
114
     * * self::DEBUG_SERVER (`2`) Client commands and server responses
115
     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
116
     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
117
     *
118
     * @var int
119
     */
120
    public $do_debug = self::DEBUG_OFF;
121
122
    /**
123
     * How to handle debug output.
124
     * Options:
125
     * * `echo` Output plain-text as-is, appropriate for CLI
126
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
127
     * * `error_log` Output to error log as configured in php.ini
128
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
129
     *
130
     * ```php
131
     * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
132
     * ```
133
     *
134
     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
135
     * level output is used:
136
     *
137
     * ```php
138
     * $mail->Debugoutput = new myPsr3Logger;
139
     * ```
140
     *
141
     * @var string|callable|\Psr\Log\LoggerInterface
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...
142
     */
143
    public $Debugoutput = 'echo';
144
145
    /**
146
     * Whether to use VERP.
147
     *
148
     * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
149
     * @see http://www.postfix.org/VERP_README.html Info on VERP
150
     *
151
     * @var bool
152
     */
153
    public $do_verp = false;
154
155
    /**
156
     * The timeout value for connection, in seconds.
157
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
158
     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
159
     *
160
     * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
161
     *
162
     * @var int
163
     */
164
    public $Timeout = 300;
165
166
    /**
167
     * How long to wait for commands to complete, in seconds.
168
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
169
     *
170
     * @var int
171
     */
172
    public $Timelimit = 300;
173
174
    /**
175
     * Patterns to extract an SMTP transaction id from reply to a DATA command.
176
     * The first capture group in each regex will be used as the ID.
177
     * MS ESMTP returns the message ID, which may not be correct for internal tracking.
178
     *
179
     * @var string[]
180
     */
181
    protected $smtp_transaction_id_patterns = [
182
        'exim' => '/[\d]{3} OK id=(.*)/',
183
        'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/',
184
        'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/',
185
        'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/',
186
        'Amazon_SES' => '/[\d]{3} Ok (.*)/',
187
        'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
188
        'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
189
    ];
190
191
    /**
192
     * The last transaction ID issued in response to a DATA command,
193
     * if one was detected.
194
     *
195
     * @var string|bool|null
196
     */
197
    protected $last_smtp_transaction_id;
198
199
    /**
200
     * The socket for the server connection.
201
     *
202
     * @var ?resource
203
     */
204
    protected $smtp_conn;
205
206
    /**
207
     * Error information, if any, for the last SMTP command.
208
     *
209
     * @var array
210
     */
211
    protected $error = [
212
        'error' => '',
213
        'detail' => '',
214
        'smtp_code' => '',
215
        'smtp_code_ex' => '',
216
    ];
217
218
    /**
219
     * The reply the server sent to us for HELO.
220
     * If null, no HELO string has yet been received.
221
     *
222
     * @var string|null
223
     */
224
    protected $helo_rply;
225
226
    /**
227
     * The set of SMTP extensions sent in reply to EHLO command.
228
     * Indexes of the array are extension names.
229
     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
230
     * represents the server name. In case of HELO it is the only element of the array.
231
     * Other values can be boolean TRUE or an array containing extension options.
232
     * If null, no HELO/EHLO string has yet been received.
233
     *
234
     * @var array|null
235
     */
236
    protected $server_caps;
237
238
    /**
239
     * The most recent reply received from the server.
240
     *
241
     * @var string
242
     */
243
    protected $last_reply = '';
244
245
    /**
246
     * Output debugging info via a user-selected method.
247
     *
248
     * @param string $str   Debug string to output
249
     * @param int    $level The debug level of this message; see DEBUG_* constants
250
     *
251
     * @see SMTP::$Debugoutput
252
     * @see SMTP::$do_debug
253
     */
254
    protected function edebug($str, $level = 0)
255
    {
256
        if ($level > $this->do_debug) {
257
            return;
258
        }
259
        //Is this a PSR-3 logger?
260
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
261
            $this->Debugoutput->debug($str);
262
263
            return;
264
        }
265
        //Avoid clash with built-in function names
266
        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
267
            call_user_func($this->Debugoutput, $str, $level);
268
269
            return;
270
        }
271
        switch ($this->Debugoutput) {
272
            case 'error_log':
273
                //Don't output, just log
274
                error_log($str);
275
                break;
276
            case 'html':
277
                //Cleans up output a bit for a better looking, HTML-safe output
278
                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
279
                    preg_replace('/[\r\n]+/', '', $str),
280
                    ENT_QUOTES,
281
                    'UTF-8'
282
                ), "<br>\n";
283
                break;
284
            case 'echo':
285
            default:
286
                //Normalize line breaks
287
                $str = preg_replace('/\r\n|\r/m', "\n", $str);
288
                echo gmdate('Y-m-d H:i:s'),
289
                "\t",
290
                    //Trim trailing space
291
                trim(
292
                    //Indent for readability, except for trailing break
293
                    str_replace(
294
                        "\n",
295
                        "\n                   \t                  ",
296
                        trim($str)
297
                    )
298
                ),
299
                "\n";
300
        }
301
    }
302
303
    /**
304
     * Connect to an SMTP server.
305
     *
306
     * @param string $host    SMTP server IP or host name
307
     * @param int    $port    The port number to connect to
308
     * @param int    $timeout How long to wait for the connection to open
309
     * @param array  $options An array of options for stream_context_create()
310
     *
311
     * @return bool
312
     */
313
    public function connect($host, $port = null, $timeout = 30, $options = [])
314
    {
315
        //Clear errors to avoid confusion
316
        $this->setError('');
317
        //Make sure we are __not__ connected
318
        if ($this->connected()) {
319
            //Already connected, generate error
320
            $this->setError('Already connected to a server');
321
322
            return false;
323
        }
324
        if (empty($port)) {
325
            $port = self::DEFAULT_PORT;
326
        }
327
        //Connect to the SMTP server
328
        $this->edebug(
329
            "Connection: opening to $host:$port, timeout=$timeout, options=" .
330
            (count($options) > 0 ? var_export($options, true) : 'array()'),
331
            self::DEBUG_CONNECTION
332
        );
333
334
        $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
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...
335
336
        if ($this->smtp_conn === false) {
337
            //Error info already set inside `getSMTPConnection()`
338
            return false;
339
        }
340
341
        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
342
343
        //Get any announcement
344
        $this->last_reply = $this->get_lines();
345
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
346
        $responseCode = (int)substr($this->last_reply, 0, 3);
347
        if ($responseCode === 220) {
348
            return true;
349
        }
350
        //Anything other than a 220 response means something went wrong
351
        //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
352
        //https://tools.ietf.org/html/rfc5321#section-3.1
353
        if ($responseCode === 554) {
354
            $this->quit();
355
        }
356
        //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
357
        $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
358
        $this->close();
359
        return false;
360
    }
361
362
    /**
363
     * Create connection to the SMTP server.
364
     *
365
     * @param string $host    SMTP server IP or host name
366
     * @param int    $port    The port number to connect to
367
     * @param int    $timeout How long to wait for the connection to open
368
     * @param array  $options An array of options for stream_context_create()
369
     *
370
     * @return false|resource
371
     */
372
    protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
373
    {
374
        static $streamok;
375
        //This is enabled by default since 5.0.0 but some providers disable it
376
        //Check this once and cache the result
377
        if (null === $streamok) {
378
            $streamok = function_exists('stream_socket_client');
379
        }
380
381
        $errno = 0;
382
        $errstr = '';
383
        if ($streamok) {
384
            $socket_context = stream_context_create($options);
385
            set_error_handler([$this, 'errorHandler']);
386
            $connection = stream_socket_client(
387
                $host . ':' . $port,
388
                $errno,
389
                $errstr,
390
                $timeout,
391
                STREAM_CLIENT_CONNECT,
392
                $socket_context
393
            );
394
            restore_error_handler();
395
        } else {
396
            //Fall back to fsockopen which should work in more places, but is missing some features
397
            $this->edebug(
398
                'Connection: stream_socket_client not available, falling back to fsockopen',
399
                self::DEBUG_CONNECTION
400
            );
401
            set_error_handler([$this, 'errorHandler']);
402
            $connection = fsockopen(
403
                $host,
404
                $port,
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

404
                /** @scrutinizer ignore-type */ $port,
Loading history...
405
                $errno,
406
                $errstr,
407
                $timeout
408
            );
409
            restore_error_handler();
410
        }
411
412
        //Verify we connected properly
413
        if (!is_resource($connection)) {
414
            $this->setError(
415
                'Failed to connect to server',
416
                '',
417
                (string) $errno,
418
                $errstr
419
            );
420
            $this->edebug(
421
                'SMTP ERROR: ' . $this->error['error']
422
                . ": $errstr ($errno)",
423
                self::DEBUG_CLIENT
424
            );
425
426
            return false;
427
        }
428
429
        //SMTP server can take longer to respond, give longer timeout for first read
430
        //Windows does not have support for this timeout function
431
        if (strpos(PHP_OS, 'WIN') !== 0) {
432
            $max = (int)ini_get('max_execution_time');
433
            //Don't bother if unlimited, or if set_time_limit is disabled
434
            if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
435
                @set_time_limit($timeout);
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

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

809
        if (substr(/** @scrutinizer ignore-type */ $this->helo_rply, 0, 3) == '421') {
Loading history...
810
            return false;
811
        }
812
813
        return $this->sendHello('HELO', $host);
814
    }
815
816
    /**
817
     * Send an SMTP HELO or EHLO command.
818
     * Low-level implementation used by hello().
819
     *
820
     * @param string $hello The HELO string
821
     * @param string $host  The hostname to say we are
822
     *
823
     * @return bool
824
     *
825
     * @see hello()
826
     */
827
    protected function sendHello($hello, $host)
828
    {
829
        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
830
        $this->helo_rply = $this->last_reply;
831
        if ($noerror) {
832
            $this->parseHelloFields($hello);
833
        } else {
834
            $this->server_caps = null;
835
        }
836
837
        return $noerror;
838
    }
839
840
    /**
841
     * Parse a reply to HELO/EHLO command to discover server extensions.
842
     * In case of HELO, the only parameter that can be discovered is a server name.
843
     *
844
     * @param string $type `HELO` or `EHLO`
845
     */
846
    protected function parseHelloFields($type)
847
    {
848
        $this->server_caps = [];
849
        $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

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

1262
            $this->edebug('SMTP INBOUND: "' . trim(/** @scrutinizer ignore-type */ $str) . '"', self::DEBUG_LOWLEVEL);
Loading history...
1263
            $data .= $str;
1264
            //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
1265
            //or 4th character is a space or a line break char, we are done reading, break the loop.
1266
            //String array access is a significant micro-optimisation over strlen
1267
            if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
1268
                break;
1269
            }
1270
            //Timed-out? Log and break
1271
            $info = stream_get_meta_data($this->smtp_conn);
1272
            if ($info['timed_out']) {
1273
                $this->edebug(
1274
                    'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
1275
                    self::DEBUG_LOWLEVEL
1276
                );
1277
                break;
1278
            }
1279
            //Now check if reads took too long
1280
            if ($endtime && time() > $endtime) {
1281
                $this->edebug(
1282
                    'SMTP -> get_lines(): timelimit reached (' .
1283
                    $this->Timelimit . ' sec)',
1284
                    self::DEBUG_LOWLEVEL
1285
                );
1286
                break;
1287
            }
1288
        }
1289
1290
        return $data;
1291
    }
1292
1293
    /**
1294
     * Enable or disable VERP address generation.
1295
     *
1296
     * @param bool $enabled
1297
     */
1298
    public function setVerp($enabled = false)
1299
    {
1300
        $this->do_verp = $enabled;
1301
    }
1302
1303
    /**
1304
     * Get VERP address generation mode.
1305
     *
1306
     * @return bool
1307
     */
1308
    public function getVerp()
1309
    {
1310
        return $this->do_verp;
1311
    }
1312
1313
    /**
1314
     * Set error messages and codes.
1315
     *
1316
     * @param string $message      The error message
1317
     * @param string $detail       Further detail on the error
1318
     * @param string $smtp_code    An associated SMTP error code
1319
     * @param string $smtp_code_ex Extended SMTP code
1320
     */
1321
    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1322
    {
1323
        $this->error = [
1324
            'error' => $message,
1325
            'detail' => $detail,
1326
            'smtp_code' => $smtp_code,
1327
            'smtp_code_ex' => $smtp_code_ex,
1328
        ];
1329
    }
1330
1331
    /**
1332
     * Set debug output method.
1333
     *
1334
     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
1335
     */
1336
    public function setDebugOutput($method = 'echo')
1337
    {
1338
        $this->Debugoutput = $method;
1339
    }
1340
1341
    /**
1342
     * Get debug output method.
1343
     *
1344
     * @return string
1345
     */
1346
    public function getDebugOutput()
1347
    {
1348
        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...
1349
    }
1350
1351
    /**
1352
     * Set debug output level.
1353
     *
1354
     * @param int $level
1355
     */
1356
    public function setDebugLevel($level = 0)
1357
    {
1358
        $this->do_debug = $level;
1359
    }
1360
1361
    /**
1362
     * Get debug output level.
1363
     *
1364
     * @return int
1365
     */
1366
    public function getDebugLevel()
1367
    {
1368
        return $this->do_debug;
1369
    }
1370
1371
    /**
1372
     * Set SMTP timeout.
1373
     *
1374
     * @param int $timeout The timeout duration in seconds
1375
     */
1376
    public function setTimeout($timeout = 0)
1377
    {
1378
        $this->Timeout = $timeout;
1379
    }
1380
1381
    /**
1382
     * Get SMTP timeout.
1383
     *
1384
     * @return int
1385
     */
1386
    public function getTimeout()
1387
    {
1388
        return $this->Timeout;
1389
    }
1390
1391
    /**
1392
     * Reports an error number and string.
1393
     *
1394
     * @param int    $errno   The error number returned by PHP
1395
     * @param string $errmsg  The error message returned by PHP
1396
     * @param string $errfile The file the error occurred in
1397
     * @param int    $errline The line number the error occurred on
1398
     */
1399
    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1400
    {
1401
        $notice = 'Connection failed.';
1402
        $this->setError(
1403
            $notice,
1404
            $errmsg,
1405
            (string) $errno
1406
        );
1407
        $this->edebug(
1408
            "$notice Error #$errno: $errmsg [$errfile line $errline]",
1409
            self::DEBUG_CONNECTION
1410
        );
1411
    }
1412
1413
    /**
1414
     * Extract and return the ID of the last SMTP transaction based on
1415
     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1416
     * Relies on the host providing the ID in response to a DATA command.
1417
     * If no reply has been received yet, it will return null.
1418
     * If no pattern was matched, it will return false.
1419
     *
1420
     * @return bool|string|null
1421
     */
1422
    protected function recordLastTransactionID()
1423
    {
1424
        $reply = $this->getLastReply();
1425
1426
        if (empty($reply)) {
1427
            $this->last_smtp_transaction_id = null;
1428
        } else {
1429
            $this->last_smtp_transaction_id = false;
1430
            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1431
                $matches = [];
1432
                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1433
                    $this->last_smtp_transaction_id = trim($matches[1]);
1434
                    break;
1435
                }
1436
            }
1437
        }
1438
1439
        return $this->last_smtp_transaction_id;
1440
    }
1441
1442
    /**
1443
     * Get the queue/transaction ID of the last SMTP transaction
1444
     * If no reply has been received yet, it will return null.
1445
     * If no pattern was matched, it will return false.
1446
     *
1447
     * @return bool|string|null
1448
     *
1449
     * @see recordLastTransactionID()
1450
     */
1451
    public function getLastTransactionID()
1452
    {
1453
        return $this->last_smtp_transaction_id;
1454
    }
1455
}
1456