Passed
Push — master ( 79149c...dd37f9 )
by Richard
05:12 queued 11s
created

SMTP   F

Complexity

Total Complexity 134

Size/Duplication

Total Lines 1248
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 134
eloc 440
dl 0
loc 1248
rs 2
c 1
b 0
f 0

38 Methods

Rating   Name   Duplication   Size   Complexity  
A getLastTransactionID() 0 3 1
A recordLastTransactionID() 0 16 4
A close() 0 10 2
A getLastReply() 0 3 1
A sendAndMail() 0 3 1
B get_lines() 0 43 11
A verify() 0 3 1
A recipient() 0 6 1
A getDebugOutput() 0 3 1
A setVerp() 0 3 1
C data() 0 78 13
A setDebugOutput() 0 3 1
A noop() 0 3 1
A getDebugLevel() 0 3 1
A turn() 0 5 1
A setError() 0 7 1
A client_send() 0 7 1
A getTimeout() 0 3 1
B edebug() 0 32 7
A getError() 0 3 1
A getVerp() 0 3 1
B sendCommand() 0 51 8
A getServerExtList() 0 3 1
A startTLS() 0 25 3
A errorHandler() 0 11 1
A setDebugLevel() 0 3 1
A mail() 0 7 2
A reset() 0 3 1
A quit() 0 9 3
A setTimeout() 0 3 1
B parseHelloFields() 0 32 9
B connect() 0 84 9
A connected() 0 16 3
D authenticate() 0 158 26
A hello() 0 4 2
A hmac() 0 25 3
A sendHello() 0 10 2
A getServerExt() 0 20 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
 * PHPMailer RFC821 SMTP email transport class.
4
 * PHP Version 5
5
 * @package PHPMailer
6
 * @link https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
7
 * @author Marcus Bointon (Synchro/coolbru) <[email protected]>
8
 * @author Jim Jagielski (jimjag) <[email protected]>
9
 * @author Andy Prevost (codeworxtech) <[email protected]>
10
 * @author Brent R. Matzelle (original founder)
11
 * @copyright 2014 Marcus Bointon
12
 * @copyright 2010 - 2012 Jim Jagielski
13
 * @copyright 2004 - 2009 Andy Prevost
14
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
15
 * @note This program is distributed in the hope that it will be useful - WITHOUT
16
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17
 * FITNESS FOR A PARTICULAR PURPOSE.
18
 */
19
20
/**
21
 * PHPMailer RFC821 SMTP email transport class.
22
 * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
23
 * @package PHPMailer
24
 * @author Chris Ryan
25
 * @author Marcus Bointon <[email protected]>
26
 */
27
class SMTP
28
{
29
    /**
30
     * The PHPMailer SMTP version number.
31
     * @var string
32
     */
33
    const VERSION = '5.2.27';
34
35
    /**
36
     * SMTP line break constant.
37
     * @var string
38
     */
39
    const CRLF = "\r\n";
40
41
    /**
42
     * The SMTP port to use if one is not specified.
43
     * @var integer
44
     */
45
    const DEFAULT_SMTP_PORT = 25;
46
47
    /**
48
     * The maximum line length allowed by RFC 2822 section 2.1.1
49
     * @var integer
50
     */
51
    const MAX_LINE_LENGTH = 998;
52
53
    /**
54
     * Debug level for no output
55
     */
56
    const DEBUG_OFF = 0;
57
58
    /**
59
     * Debug level to show client -> server messages
60
     */
61
    const DEBUG_CLIENT = 1;
62
63
    /**
64
     * Debug level to show client -> server and server -> client messages
65
     */
66
    const DEBUG_SERVER = 2;
67
68
    /**
69
     * Debug level to show connection status, client -> server and server -> client messages
70
     */
71
    const DEBUG_CONNECTION = 3;
72
73
    /**
74
     * Debug level to show all messages
75
     */
76
    const DEBUG_LOWLEVEL = 4;
77
78
    /**
79
     * The PHPMailer SMTP Version number.
80
     * @var string
81
     * @deprecated Use the `VERSION` constant instead
82
     * @see SMTP::VERSION
83
     */
84
    public $Version = '5.2.27';
85
86
    /**
87
     * SMTP server port number.
88
     * @var integer
89
     * @deprecated This is only ever used as a default value, so use the `DEFAULT_SMTP_PORT` constant instead
90
     * @see SMTP::DEFAULT_SMTP_PORT
91
     */
92
    public $SMTP_PORT = 25;
93
94
    /**
95
     * SMTP reply line ending.
96
     * @var string
97
     * @deprecated Use the `CRLF` constant instead
98
     * @see SMTP::CRLF
99
     */
100
    public $CRLF = "\r\n";
101
102
    /**
103
     * Debug output level.
104
     * Options:
105
     * * self::DEBUG_OFF (`0`) No debug output, default
106
     * * self::DEBUG_CLIENT (`1`) Client commands
107
     * * self::DEBUG_SERVER (`2`) Client commands and server responses
108
     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
109
     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages
110
     * @var integer
111
     */
112
    public $do_debug = self::DEBUG_OFF;
113
114
    /**
115
     * How to handle debug output.
116
     * Options:
117
     * * `echo` Output plain-text as-is, appropriate for CLI
118
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
119
     * * `error_log` Output to error log as configured in php.ini
120
     *
121
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
122
     * <code>
123
     * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
124
     * </code>
125
     * @var string|callable
126
     */
127
    public $Debugoutput = 'echo';
128
129
    /**
130
     * Whether to use VERP.
131
     * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path
132
     * @link http://www.postfix.org/VERP_README.html Info on VERP
133
     * @var boolean
134
     */
135
    public $do_verp = false;
136
137
    /**
138
     * The timeout value for connection, in seconds.
139
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
140
     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
141
     * @link http://tools.ietf.org/html/rfc2821#section-4.5.3.2
142
     * @var integer
143
     */
144
    public $Timeout = 300;
145
146
    /**
147
     * How long to wait for commands to complete, in seconds.
148
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2
149
     * @var integer
150
     */
151
    public $Timelimit = 300;
152
153
    /**
154
     * @var array Patterns to extract an SMTP transaction id from reply to a DATA command.
155
     * The first capture group in each regex will be used as the ID.
156
     */
157
    protected $smtp_transaction_id_patterns = array(
158
        'exim' => '/[0-9]{3} OK id=(.*)/',
159
        'sendmail' => '/[0-9]{3} 2.0.0 (.*) Message/',
160
        'postfix' => '/[0-9]{3} 2.0.0 Ok: queued as (.*)/'
161
    );
162
163
    /**
164
     * @var string The last transaction ID issued in response to a DATA command,
165
     * if one was detected
166
     */
167
    protected $last_smtp_transaction_id;
168
169
    /**
170
     * The socket for the server connection.
171
     * @var resource
172
     */
173
    protected $smtp_conn;
174
175
    /**
176
     * Error information, if any, for the last SMTP command.
177
     * @var array
178
     */
179
    protected $error = array(
180
        'error' => '',
181
        'detail' => '',
182
        'smtp_code' => '',
183
        'smtp_code_ex' => ''
184
    );
185
186
    /**
187
     * The reply the server sent to us for HELO.
188
     * If null, no HELO string has yet been received.
189
     * @var string|null
190
     */
191
    protected $helo_rply = null;
192
193
    /**
194
     * The set of SMTP extensions sent in reply to EHLO command.
195
     * Indexes of the array are extension names.
196
     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
197
     * represents the server name. In case of HELO it is the only element of the array.
198
     * Other values can be boolean TRUE or an array containing extension options.
199
     * If null, no HELO/EHLO string has yet been received.
200
     * @var array|null
201
     */
202
    protected $server_caps = null;
203
204
    /**
205
     * The most recent reply received from the server.
206
     * @var string
207
     */
208
    protected $last_reply = '';
209
210
    /**
211
     * Output debugging info via a user-selected method.
212
     * @see SMTP::$Debugoutput
213
     * @see SMTP::$do_debug
214
     * @param string $str Debug string to output
215
     * @param integer $level The debug level of this message; see DEBUG_* constants
216
     * @return void
217
     */
218
    protected function edebug($str, $level = 0)
219
    {
220
        if ($level > $this->do_debug) {
221
            return;
222
        }
223
        //Avoid clash with built-in function names
224
        if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) {
225
            call_user_func($this->Debugoutput, $str, $level);
226
            return;
227
        }
228
        switch ($this->Debugoutput) {
229
            case 'error_log':
230
                //Don't output, just log
231
                error_log($str);
232
                break;
233
            case 'html':
234
                //Cleans up output a bit for a better looking, HTML-safe output
235
                echo gmdate('Y-m-d H:i:s') . ' ' . htmlentities(
236
                    preg_replace('/[\r\n]+/', '', $str),
237
                    ENT_QUOTES,
238
                    'UTF-8'
239
                ) . "<br>\n";
240
                break;
241
            case 'echo':
242
            default:
243
                //Normalize line breaks
244
                $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str);
245
                echo gmdate('Y-m-d H:i:s') . "\t" . str_replace(
246
                    "\n",
247
                    "\n                   \t                  ",
248
                    trim($str)
249
                ) . "\n";
250
        }
251
    }
252
253
    /**
254
     * Connect to an SMTP server.
255
     * @param string $host SMTP server IP or host name
256
     * @param integer $port The port number to connect to
257
     * @param integer $timeout How long to wait for the connection to open
258
     * @param array $options An array of options for stream_context_create()
259
     * @access public
260
     * @return boolean
261
     */
262
    public function connect($host, $port = null, $timeout = 30, $options = array())
263
    {
264
        static $streamok;
265
        //This is enabled by default since 5.0.0 but some providers disable it
266
        //Check this once and cache the result
267
        if (is_null($streamok)) {
268
            $streamok = function_exists('stream_socket_client');
269
        }
270
        // Clear errors to avoid confusion
271
        $this->setError('');
272
        // Make sure we are __not__ connected
273
        if ($this->connected()) {
274
            // Already connected, generate error
275
            $this->setError('Already connected to a server');
276
            return false;
277
        }
278
        if (empty($port)) {
279
            $port = self::DEFAULT_SMTP_PORT;
280
        }
281
        // Connect to the SMTP server
282
        $this->edebug(
283
            "Connection: opening to $host:$port, timeout=$timeout, options=" .
284
            var_export($options, true),
285
            self::DEBUG_CONNECTION
286
        );
287
        $errno = 0;
288
        $errstr = '';
289
        if ($streamok) {
290
            $socket_context = stream_context_create($options);
291
            set_error_handler(array($this, 'errorHandler'));
292
            $this->smtp_conn = stream_socket_client(
0 ignored issues
show
Documentation Bug introduced by
It seems like stream_socket_client($ho...NNECT, $socket_context) can also be of type false. However, the property $smtp_conn is declared as type 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...
293
                $host . ":" . $port,
294
                $errno,
295
                $errstr,
296
                $timeout,
297
                STREAM_CLIENT_CONNECT,
298
                $socket_context
299
            );
300
            restore_error_handler();
301
        } else {
302
            //Fall back to fsockopen which should work in more places, but is missing some features
303
            $this->edebug(
304
                "Connection: stream_socket_client not available, falling back to fsockopen",
305
                self::DEBUG_CONNECTION
306
            );
307
            set_error_handler(array($this, 'errorHandler'));
308
            $this->smtp_conn = fsockopen(
309
                $host,
310
                $port,
311
                $errno,
312
                $errstr,
313
                $timeout
314
            );
315
            restore_error_handler();
316
        }
317
        // Verify we connected properly
318
        if (!is_resource($this->smtp_conn)) {
319
            $this->setError(
320
                'Failed to connect to server',
321
                $errno,
322
                $errstr
323
            );
324
            $this->edebug(
325
                'SMTP ERROR: ' . $this->error['error']
326
                . ": $errstr ($errno)",
327
                self::DEBUG_CLIENT
328
            );
329
            return false;
330
        }
331
        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
332
        // SMTP server can take longer to respond, give longer timeout for first read
333
        // Windows does not have support for this timeout function
334
        if (substr(PHP_OS, 0, 3) != 'WIN') {
335
            $max = ini_get('max_execution_time');
336
            // Don't bother if unlimited
337
            if ($max != 0 && $timeout > $max) {
338
                @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

338
                /** @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...
339
            }
340
            stream_set_timeout($this->smtp_conn, $timeout, 0);
341
        }
342
        // Get any announcement
343
        $announce = $this->get_lines();
344
        $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
345
        return true;
346
    }
347
348
    /**
349
     * Initiate a TLS (encrypted) session.
350
     * @access public
351
     * @return boolean
352
     */
353
    public function startTLS()
354
    {
355
        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
356
            return false;
357
        }
358
359
        //Allow the best TLS version(s) we can
360
        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
361
362
        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
363
        //so add them back in manually if we can
364
        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
365
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
366
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
367
        }
368
369
        // Begin encrypted connection
370
        set_error_handler(array($this, 'errorHandler'));
371
        $crypto_ok = stream_socket_enable_crypto(
372
            $this->smtp_conn,
373
            true,
374
            $crypto_method
375
        );
376
        restore_error_handler();
377
        return $crypto_ok;
378
    }
379
380
    /**
381
     * Perform SMTP authentication.
382
     * Must be run after hello().
383
     * @see hello()
384
     * @param string $username The user name
385
     * @param string $password The password
386
     * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5, XOAUTH2)
387
     * @param string $realm The auth realm for NTLM
388
     * @param string $workstation The auth workstation for NTLM
389
     * @param null|OAuth $OAuth An optional OAuth instance (@see PHPMailerOAuth)
390
     * @return bool True if successfully authenticated.* @access public
391
     */
392
    public function authenticate(
393
        $username,
394
        $password,
395
        $authtype = null,
396
        $realm = '',
397
        $workstation = '',
398
        $OAuth = null
399
    ) {
400
        if (!$this->server_caps) {
401
            $this->setError('Authentication is not allowed before HELO/EHLO');
402
            return false;
403
        }
404
405
        if (array_key_exists('EHLO', $this->server_caps)) {
406
            // SMTP extensions are available; try to find a proper authentication method
407
            if (!array_key_exists('AUTH', $this->server_caps)) {
408
                $this->setError('Authentication is not allowed at this stage');
409
                // 'at this stage' means that auth may be allowed after the stage changes
410
                // e.g. after STARTTLS
411
                return false;
412
            }
413
414
            self::edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL);
0 ignored issues
show
Bug Best Practice introduced by
The method SMTP::edebug() is not static, but was called statically. ( Ignorable by Annotation )

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

414
            self::/** @scrutinizer ignore-call */ 
415
                  edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL);
Loading history...
415
            self::edebug(
416
                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
417
                self::DEBUG_LOWLEVEL
418
            );
419
420
            if (empty($authtype)) {
421
                foreach (array('CRAM-MD5', 'LOGIN', 'PLAIN', 'NTLM', 'XOAUTH2') as $method) {
422
                    if (in_array($method, $this->server_caps['AUTH'])) {
423
                        $authtype = $method;
424
                        break;
425
                    }
426
                }
427
                if (empty($authtype)) {
428
                    $this->setError('No supported authentication methods found');
429
                    return false;
430
                }
431
                self::edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
432
            }
433
434
            if (!in_array($authtype, $this->server_caps['AUTH'])) {
435
                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
436
                return false;
437
            }
438
        } elseif (empty($authtype)) {
439
            $authtype = 'LOGIN';
440
        }
441
        switch ($authtype) {
442
            case 'PLAIN':
443
                // Start authentication
444
                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
445
                    return false;
446
                }
447
                // Send encoded username and password
448
                if (!$this->sendCommand(
449
                    'User & Password',
450
                    base64_encode("\0" . $username . "\0" . $password),
451
                    235
452
                )
453
                ) {
454
                    return false;
455
                }
456
                break;
457
            case 'LOGIN':
458
                // Start authentication
459
                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
460
                    return false;
461
                }
462
                if (!$this->sendCommand("Username", base64_encode($username), 334)) {
463
                    return false;
464
                }
465
                if (!$this->sendCommand("Password", base64_encode($password), 235)) {
466
                    return false;
467
                }
468
                break;
469
            case 'XOAUTH2':
470
                //If the OAuth Instance is not set. Can be a case when PHPMailer is used
471
                //instead of PHPMailerOAuth
472
                if (is_null($OAuth)) {
473
                    return false;
474
                }
475
                $oauth = $OAuth->getOauth64();
0 ignored issues
show
Bug introduced by
The method getOauth64() does not exist on OAuth. ( Ignorable by Annotation )

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

475
                /** @scrutinizer ignore-call */ 
476
                $oauth = $OAuth->getOauth64();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
476
477
                // Start authentication
478
                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
479
                    return false;
480
                }
481
                break;
482
            case 'NTLM':
483
                /*
484
                 * ntlm_sasl_client.php
485
                 * Bundled with Permission
486
                 *
487
                 * How to telnet in windows:
488
                 * http://technet.microsoft.com/en-us/library/aa995718%28EXCHG.65%29.aspx
489
                 * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication
490
                 */
491
                require_once 'extras/ntlm_sasl_client.php';
492
                $temp = new stdClass;
493
                $ntlm_client = new ntlm_sasl_client_class;
0 ignored issues
show
Bug introduced by
The type ntlm_sasl_client_class 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...
494
                //Check that functions are available
495
                if (!$ntlm_client->initialize($temp)) {
496
                    $this->setError($temp->error);
497
                    $this->edebug(
498
                        'You need to enable some modules in your php.ini file: '
499
                        . $this->error['error'],
500
                        self::DEBUG_CLIENT
501
                    );
502
                    return false;
503
                }
504
                //msg1
505
                $msg1 = $ntlm_client->typeMsg1($realm, $workstation); //msg1
506
507
                if (!$this->sendCommand(
508
                    'AUTH NTLM',
509
                    'AUTH NTLM ' . base64_encode($msg1),
510
                    334
511
                )
512
                ) {
513
                    return false;
514
                }
515
                //Though 0 based, there is a white space after the 3 digit number
516
                //msg2
517
                $challenge = substr($this->last_reply, 3);
518
                $challenge = base64_decode($challenge);
519
                $ntlm_res = $ntlm_client->NTLMResponse(
520
                    substr($challenge, 24, 8),
521
                    $password
522
                );
523
                //msg3
524
                $msg3 = $ntlm_client->typeMsg3(
525
                    $ntlm_res,
526
                    $username,
527
                    $realm,
528
                    $workstation
529
                );
530
                // send encoded username
531
                return $this->sendCommand('Username', base64_encode($msg3), 235);
532
            case 'CRAM-MD5':
533
                // Start authentication
534
                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
535
                    return false;
536
                }
537
                // Get the challenge
538
                $challenge = base64_decode(substr($this->last_reply, 4));
539
540
                // Build the response
541
                $response = $username . ' ' . $this->hmac($challenge, $password);
542
543
                // send encoded credentials
544
                return $this->sendCommand('Username', base64_encode($response), 235);
545
            default:
546
                $this->setError("Authentication method \"$authtype\" is not supported");
547
                return false;
548
        }
549
        return true;
550
    }
551
552
    /**
553
     * Calculate an MD5 HMAC hash.
554
     * Works like hash_hmac('md5', $data, $key)
555
     * in case that function is not available
556
     * @param string $data The data to hash
557
     * @param string $key The key to hash with
558
     * @access protected
559
     * @return string
560
     */
561
    protected function hmac($data, $key)
562
    {
563
        if (function_exists('hash_hmac')) {
564
            return hash_hmac('md5', $data, $key);
565
        }
566
567
        // The following borrowed from
568
        // http://php.net/manual/en/function.mhash.php#27225
569
570
        // RFC 2104 HMAC implementation for php.
571
        // Creates an md5 HMAC.
572
        // Eliminates the need to install mhash to compute a HMAC
573
        // by Lance Rushing
574
575
        $bytelen = 64; // byte length for md5
576
        if (strlen($key) > $bytelen) {
577
            $key = pack('H*', md5($key));
578
        }
579
        $key = str_pad($key, $bytelen, chr(0x00));
580
        $ipad = str_pad('', $bytelen, chr(0x36));
581
        $opad = str_pad('', $bytelen, chr(0x5c));
582
        $k_ipad = $key ^ $ipad;
583
        $k_opad = $key ^ $opad;
584
585
        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
586
    }
587
588
    /**
589
     * Check connection state.
590
     * @access public
591
     * @return boolean True if connected.
592
     */
593
    public function connected()
594
    {
595
        if (is_resource($this->smtp_conn)) {
596
            $sock_status = stream_get_meta_data($this->smtp_conn);
597
            if ($sock_status['eof']) {
598
                // The socket is valid but we are not connected
599
                $this->edebug(
600
                    'SMTP NOTICE: EOF caught while checking if connected',
601
                    self::DEBUG_CLIENT
602
                );
603
                $this->close();
604
                return false;
605
            }
606
            return true; // everything looks good
607
        }
608
        return false;
609
    }
610
611
    /**
612
     * Close the socket and clean up the state of the class.
613
     * Don't use this function without first trying to use QUIT.
614
     * @see quit()
615
     * @access public
616
     * @return void
617
     */
618
    public function close()
619
    {
620
        $this->setError('');
621
        $this->server_caps = null;
622
        $this->helo_rply = null;
623
        if (is_resource($this->smtp_conn)) {
624
            // close the connection and cleanup
625
            fclose($this->smtp_conn);
626
            $this->smtp_conn = null; //Makes for cleaner serialization
627
            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
628
        }
629
    }
630
631
    /**
632
     * Send an SMTP DATA command.
633
     * Issues a data command and sends the msg_data to the server,
634
     * finializing the mail transaction. $msg_data is the message
635
     * that is to be send with the headers. Each header needs to be
636
     * on a single line followed by a <CRLF> with the message headers
637
     * and the message body being separated by and additional <CRLF>.
638
     * Implements rfc 821: DATA <CRLF>
639
     * @param string $msg_data Message data to send
640
     * @access public
641
     * @return boolean
642
     */
643
    public function data($msg_data)
644
    {
645
        //This will use the standard timelimit
646
        if (!$this->sendCommand('DATA', 'DATA', 354)) {
647
            return false;
648
        }
649
650
        /* The server is ready to accept data!
651
         * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF)
652
         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
653
         * smaller lines to fit within the limit.
654
         * We will also look for lines that start with a '.' and prepend an additional '.'.
655
         * NOTE: this does not count towards line-length limit.
656
         */
657
658
        // Normalize line breaks before exploding
659
        $lines = explode("\n", str_replace(array("\r\n", "\r"), "\n", $msg_data));
660
661
        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
662
         * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
663
         * process all lines before a blank line as headers.
664
         */
665
666
        $field = substr($lines[0], 0, strpos($lines[0], ':'));
667
        $in_headers = false;
668
        if (!empty($field) && strpos($field, ' ') === false) {
669
            $in_headers = true;
670
        }
671
672
        foreach ($lines as $line) {
673
            $lines_out = array();
674
            if ($in_headers and $line == '') {
675
                $in_headers = false;
676
            }
677
            //Break this line up into several smaller lines if it's too long
678
            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
679
            while (isset($line[self::MAX_LINE_LENGTH])) {
680
                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
681
                //so as to avoid breaking in the middle of a word
682
                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
683
                //Deliberately matches both false and 0
684
                if (!$pos) {
685
                    //No nice break found, add a hard break
686
                    $pos = self::MAX_LINE_LENGTH - 1;
687
                    $lines_out[] = substr($line, 0, $pos);
688
                    $line = substr($line, $pos);
689
                } else {
690
                    //Break at the found point
691
                    $lines_out[] = substr($line, 0, $pos);
692
                    //Move along by the amount we dealt with
693
                    $line = substr($line, $pos + 1);
694
                }
695
                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
696
                if ($in_headers) {
697
                    $line = "\t" . $line;
698
                }
699
            }
700
            $lines_out[] = $line;
701
702
            //Send the lines to the server
703
            foreach ($lines_out as $line_out) {
704
                //RFC2821 section 4.5.2
705
                if (!empty($line_out) and $line_out[0] == '.') {
706
                    $line_out = '.' . $line_out;
707
                }
708
                $this->client_send($line_out . self::CRLF);
709
            }
710
        }
711
712
        //Message data has been sent, complete the command
713
        //Increase timelimit for end of DATA command
714
        $savetimelimit = $this->Timelimit;
715
        $this->Timelimit = $this->Timelimit * 2;
716
        $result = $this->sendCommand('DATA END', '.', 250);
717
        $this->recordLastTransactionID();
718
        //Restore timelimit
719
        $this->Timelimit = $savetimelimit;
720
        return $result;
721
    }
722
723
    /**
724
     * Send an SMTP HELO or EHLO command.
725
     * Used to identify the sending server to the receiving server.
726
     * This makes sure that client and server are in a known state.
727
     * Implements RFC 821: HELO <SP> <domain> <CRLF>
728
     * and RFC 2821 EHLO.
729
     * @param string $host The host name or IP to connect to
730
     * @access public
731
     * @return boolean
732
     */
733
    public function hello($host = '')
734
    {
735
        //Try extended hello first (RFC 2821)
736
        return (boolean)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host));
737
    }
738
739
    /**
740
     * Send an SMTP HELO or EHLO command.
741
     * Low-level implementation used by hello()
742
     * @see hello()
743
     * @param string $hello The HELO string
744
     * @param string $host The hostname to say we are
745
     * @access protected
746
     * @return boolean
747
     */
748
    protected function sendHello($hello, $host)
749
    {
750
        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
751
        $this->helo_rply = $this->last_reply;
752
        if ($noerror) {
753
            $this->parseHelloFields($hello);
754
        } else {
755
            $this->server_caps = null;
756
        }
757
        return $noerror;
758
    }
759
760
    /**
761
     * Parse a reply to HELO/EHLO command to discover server extensions.
762
     * In case of HELO, the only parameter that can be discovered is a server name.
763
     * @access protected
764
     * @param string $type - 'HELO' or 'EHLO'
765
     */
766
    protected function parseHelloFields($type)
767
    {
768
        $this->server_caps = array();
769
        $lines = explode("\n", $this->helo_rply);
770
771
        foreach ($lines as $n => $s) {
772
            //First 4 chars contain response code followed by - or space
773
            $s = trim(substr($s, 4));
774
            if (empty($s)) {
775
                continue;
776
            }
777
            $fields = explode(' ', $s);
778
            if (!empty($fields)) {
779
                if (!$n) {
780
                    $name = $type;
781
                    $fields = $fields[0];
782
                } else {
783
                    $name = array_shift($fields);
784
                    switch ($name) {
785
                        case 'SIZE':
786
                            $fields = ($fields ? $fields[0] : 0);
787
                            break;
788
                        case 'AUTH':
789
                            if (!is_array($fields)) {
790
                                $fields = array();
791
                            }
792
                            break;
793
                        default:
794
                            $fields = true;
795
                    }
796
                }
797
                $this->server_caps[$name] = $fields;
798
            }
799
        }
800
    }
801
802
    /**
803
     * Send an SMTP MAIL command.
804
     * Starts a mail transaction from the email address specified in
805
     * $from. Returns true if successful or false otherwise. If True
806
     * the mail transaction is started and then one or more recipient
807
     * commands may be called followed by a data command.
808
     * Implements rfc 821: MAIL <SP> FROM:<reverse-path> <CRLF>
809
     * @param string $from Source address of this message
810
     * @access public
811
     * @return boolean
812
     */
813
    public function mail($from)
814
    {
815
        $useVerp = ($this->do_verp ? ' XVERP' : '');
816
        return $this->sendCommand(
817
            'MAIL FROM',
818
            'MAIL FROM:<' . $from . '>' . $useVerp,
819
            250
820
        );
821
    }
822
823
    /**
824
     * Send an SMTP QUIT command.
825
     * Closes the socket if there is no error or the $close_on_error argument is true.
826
     * Implements from rfc 821: QUIT <CRLF>
827
     * @param boolean $close_on_error Should the connection close if an error occurs?
828
     * @access public
829
     * @return boolean
830
     */
831
    public function quit($close_on_error = true)
832
    {
833
        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
834
        $err = $this->error; //Save any error
835
        if ($noerror or $close_on_error) {
836
            $this->close();
837
            $this->error = $err; //Restore any error from the quit command
838
        }
839
        return $noerror;
840
    }
841
842
    /**
843
     * Send an SMTP RCPT command.
844
     * Sets the TO argument to $toaddr.
845
     * Returns true if the recipient was accepted false if it was rejected.
846
     * Implements from rfc 821: RCPT <SP> TO:<forward-path> <CRLF>
847
     * @param string $address The address the message is being sent to
848
     * @access public
849
     * @return boolean
850
     */
851
    public function recipient($address)
852
    {
853
        return $this->sendCommand(
854
            'RCPT TO',
855
            'RCPT TO:<' . $address . '>',
856
            array(250, 251)
857
        );
858
    }
859
860
    /**
861
     * Send an SMTP RSET command.
862
     * Abort any transaction that is currently in progress.
863
     * Implements rfc 821: RSET <CRLF>
864
     * @access public
865
     * @return boolean True on success.
866
     */
867
    public function reset()
868
    {
869
        return $this->sendCommand('RSET', 'RSET', 250);
870
    }
871
872
    /**
873
     * Send a command to an SMTP server and check its return code.
874
     * @param string $command The command name - not sent to the server
875
     * @param string $commandstring The actual command to send
876
     * @param integer|array $expect One or more expected integer success codes
877
     * @access protected
878
     * @return boolean True on success.
879
     */
880
    protected function sendCommand($command, $commandstring, $expect)
881
    {
882
        if (!$this->connected()) {
883
            $this->setError("Called $command without being connected");
884
            return false;
885
        }
886
        //Reject line breaks in all commands
887
        if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) {
888
            $this->setError("Command '$command' contained line breaks");
889
            return false;
890
        }
891
        $this->client_send($commandstring . self::CRLF);
892
893
        $this->last_reply = $this->get_lines();
894
        // Fetch SMTP code and possible error code explanation
895
        $matches = array();
896
        if (preg_match("/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches)) {
897
            $code = $matches[1];
898
            $code_ex = (count($matches) > 2 ? $matches[2] : null);
899
            // Cut off error code from each response line
900
            $detail = preg_replace(
901
                "/{$code}[ -]" .
902
                ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . "/m",
903
                '',
904
                $this->last_reply
905
            );
906
        } else {
907
            // Fall back to simple parsing if regex fails
908
            $code = substr($this->last_reply, 0, 3);
909
            $code_ex = null;
910
            $detail = substr($this->last_reply, 4);
911
        }
912
913
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
914
915
        if (!in_array($code, (array)$expect)) {
916
            $this->setError(
917
                "$command command failed",
918
                $detail,
919
                $code,
920
                $code_ex
921
            );
922
            $this->edebug(
923
                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
924
                self::DEBUG_CLIENT
925
            );
926
            return false;
927
        }
928
929
        $this->setError('');
930
        return true;
931
    }
932
933
    /**
934
     * Send an SMTP SAML command.
935
     * Starts a mail transaction from the email address specified in $from.
936
     * Returns true if successful or false otherwise. If True
937
     * the mail transaction is started and then one or more recipient
938
     * commands may be called followed by a data command. This command
939
     * will send the message to the users terminal if they are logged
940
     * in and send them an email.
941
     * Implements rfc 821: SAML <SP> FROM:<reverse-path> <CRLF>
942
     * @param string $from The address the message is from
943
     * @access public
944
     * @return boolean
945
     */
946
    public function sendAndMail($from)
947
    {
948
        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
949
    }
950
951
    /**
952
     * Send an SMTP VRFY command.
953
     * @param string $name The name to verify
954
     * @access public
955
     * @return boolean
956
     */
957
    public function verify($name)
958
    {
959
        return $this->sendCommand('VRFY', "VRFY $name", array(250, 251));
960
    }
961
962
    /**
963
     * Send an SMTP NOOP command.
964
     * Used to keep keep-alives alive, doesn't actually do anything
965
     * @access public
966
     * @return boolean
967
     */
968
    public function noop()
969
    {
970
        return $this->sendCommand('NOOP', 'NOOP', 250);
971
    }
972
973
    /**
974
     * Send an SMTP TURN command.
975
     * This is an optional command for SMTP that this class does not support.
976
     * This method is here to make the RFC821 Definition complete for this class
977
     * and _may_ be implemented in future
978
     * Implements from rfc 821: TURN <CRLF>
979
     * @access public
980
     * @return boolean
981
     */
982
    public function turn()
983
    {
984
        $this->setError('The SMTP TURN command is not implemented');
985
        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
986
        return false;
987
    }
988
989
    /**
990
     * Send raw data to the server.
991
     * @param string $data The data to send
992
     * @access public
993
     * @return integer|boolean The number of bytes sent to the server or false on error
994
     */
995
    public function client_send($data)
996
    {
997
        $this->edebug("CLIENT -> SERVER: $data", self::DEBUG_CLIENT);
998
        set_error_handler(array($this, 'errorHandler'));
999
        $result = fwrite($this->smtp_conn, $data);
1000
        restore_error_handler();
1001
        return $result;
1002
    }
1003
1004
    /**
1005
     * Get the latest error.
1006
     * @access public
1007
     * @return array
1008
     */
1009
    public function getError()
1010
    {
1011
        return $this->error;
1012
    }
1013
1014
    /**
1015
     * Get SMTP extensions available on the server
1016
     * @access public
1017
     * @return array|null
1018
     */
1019
    public function getServerExtList()
1020
    {
1021
        return $this->server_caps;
1022
    }
1023
1024
    /**
1025
     * A multipurpose method
1026
     * The method works in three ways, dependent on argument value and current state
1027
     *   1. HELO/EHLO was not sent - returns null and set up $this->error
1028
     *   2. HELO was sent
1029
     *     $name = 'HELO': returns server name
1030
     *     $name = 'EHLO': returns boolean false
1031
     *     $name = any string: returns null and set up $this->error
1032
     *   3. EHLO was sent
1033
     *     $name = 'HELO'|'EHLO': returns server name
1034
     *     $name = any string: if extension $name exists, returns boolean True
1035
     *       or its options. Otherwise returns boolean False
1036
     * In other words, one can use this method to detect 3 conditions:
1037
     *  - null returned: handshake was not or we don't know about ext (refer to $this->error)
1038
     *  - false returned: the requested feature exactly not exists
1039
     *  - positive value returned: the requested feature exists
1040
     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
1041
     * @return mixed
1042
     */
1043
    public function getServerExt($name)
1044
    {
1045
        if (!$this->server_caps) {
1046
            $this->setError('No HELO/EHLO was sent');
1047
            return null;
1048
        }
1049
1050
        // the tight logic knot ;)
1051
        if (!array_key_exists($name, $this->server_caps)) {
1052
            if ($name == 'HELO') {
1053
                return $this->server_caps['EHLO'];
1054
            }
1055
            if ($name == 'EHLO' || array_key_exists('EHLO', $this->server_caps)) {
1056
                return false;
1057
            }
1058
            $this->setError('HELO handshake was used. Client knows nothing about server extensions');
1059
            return null;
1060
        }
1061
1062
        return $this->server_caps[$name];
1063
    }
1064
1065
    /**
1066
     * Get the last reply from the server.
1067
     * @access public
1068
     * @return string
1069
     */
1070
    public function getLastReply()
1071
    {
1072
        return $this->last_reply;
1073
    }
1074
1075
    /**
1076
     * Read the SMTP server's response.
1077
     * Either before eof or socket timeout occurs on the operation.
1078
     * With SMTP we can tell if we have more lines to read if the
1079
     * 4th character is '-' symbol. If it is a space then we don't
1080
     * need to read anything else.
1081
     * @access protected
1082
     * @return string
1083
     */
1084
    protected function get_lines()
1085
    {
1086
        // If the connection is bad, give up straight away
1087
        if (!is_resource($this->smtp_conn)) {
1088
            return '';
1089
        }
1090
        $data = '';
1091
        $endtime = 0;
1092
        stream_set_timeout($this->smtp_conn, $this->Timeout);
1093
        if ($this->Timelimit > 0) {
1094
            $endtime = time() + $this->Timelimit;
1095
        }
1096
        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1097
            $str = @fgets($this->smtp_conn, 515);
1098
            $this->edebug("SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL);
1099
            $this->edebug("SMTP -> get_lines(): \$str is  \"$str\"", self::DEBUG_LOWLEVEL);
1100
            $data .= $str;
1101
            // If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
1102
            // or 4th character is a space, we are done reading, break the loop,
1103
            // string array access is a micro-optimisation over strlen
1104
            if (!isset($str[3]) or (isset($str[3]) and $str[3] == ' ')) {
1105
                break;
1106
            }
1107
            // Timed-out? Log and break
1108
            $info = stream_get_meta_data($this->smtp_conn);
1109
            if ($info['timed_out']) {
1110
                $this->edebug(
1111
                    'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)',
1112
                    self::DEBUG_LOWLEVEL
1113
                );
1114
                break;
1115
            }
1116
            // Now check if reads took too long
1117
            if ($endtime and time() > $endtime) {
1118
                $this->edebug(
1119
                    'SMTP -> get_lines(): timelimit reached (' .
1120
                    $this->Timelimit . ' sec)',
1121
                    self::DEBUG_LOWLEVEL
1122
                );
1123
                break;
1124
            }
1125
        }
1126
        return $data;
1127
    }
1128
1129
    /**
1130
     * Enable or disable VERP address generation.
1131
     * @param boolean $enabled
1132
     */
1133
    public function setVerp($enabled = false)
1134
    {
1135
        $this->do_verp = $enabled;
1136
    }
1137
1138
    /**
1139
     * Get VERP address generation mode.
1140
     * @return boolean
1141
     */
1142
    public function getVerp()
1143
    {
1144
        return $this->do_verp;
1145
    }
1146
1147
    /**
1148
     * Set error messages and codes.
1149
     * @param string $message The error message
1150
     * @param string $detail Further detail on the error
1151
     * @param string $smtp_code An associated SMTP error code
1152
     * @param string $smtp_code_ex Extended SMTP code
1153
     */
1154
    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1155
    {
1156
        $this->error = array(
1157
            'error' => $message,
1158
            'detail' => $detail,
1159
            'smtp_code' => $smtp_code,
1160
            'smtp_code_ex' => $smtp_code_ex
1161
        );
1162
    }
1163
1164
    /**
1165
     * Set debug output method.
1166
     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it.
1167
     */
1168
    public function setDebugOutput($method = 'echo')
1169
    {
1170
        $this->Debugoutput = $method;
1171
    }
1172
1173
    /**
1174
     * Get debug output method.
1175
     * @return string
1176
     */
1177
    public function getDebugOutput()
1178
    {
1179
        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...
1180
    }
1181
1182
    /**
1183
     * Set debug output level.
1184
     * @param integer $level
1185
     */
1186
    public function setDebugLevel($level = 0)
1187
    {
1188
        $this->do_debug = $level;
1189
    }
1190
1191
    /**
1192
     * Get debug output level.
1193
     * @return integer
1194
     */
1195
    public function getDebugLevel()
1196
    {
1197
        return $this->do_debug;
1198
    }
1199
1200
    /**
1201
     * Set SMTP timeout.
1202
     * @param integer $timeout
1203
     */
1204
    public function setTimeout($timeout = 0)
1205
    {
1206
        $this->Timeout = $timeout;
1207
    }
1208
1209
    /**
1210
     * Get SMTP timeout.
1211
     * @return integer
1212
     */
1213
    public function getTimeout()
1214
    {
1215
        return $this->Timeout;
1216
    }
1217
1218
    /**
1219
     * Reports an error number and string.
1220
     * @param integer $errno The error number returned by PHP.
1221
     * @param string $errmsg The error message returned by PHP.
1222
     * @param string $errfile The file the error occurred in
1223
     * @param integer $errline The line number the error occurred on
1224
     */
1225
    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1226
    {
1227
        $notice = 'Connection failed.';
1228
        $this->setError(
1229
            $notice,
1230
            $errno,
1231
            $errmsg
1232
        );
1233
        $this->edebug(
1234
            $notice . ' Error #' . $errno . ': ' . $errmsg . " [$errfile line $errline]",
1235
            self::DEBUG_CONNECTION
1236
        );
1237
    }
1238
1239
    /**
1240
     * Extract and return the ID of the last SMTP transaction based on
1241
     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1242
     * Relies on the host providing the ID in response to a DATA command.
1243
     * If no reply has been received yet, it will return null.
1244
     * If no pattern was matched, it will return false.
1245
     * @return bool|null|string
1246
     */
1247
    protected function recordLastTransactionID()
1248
    {
1249
        $reply = $this->getLastReply();
1250
1251
        if (empty($reply)) {
1252
            $this->last_smtp_transaction_id = null;
1253
        } else {
1254
            $this->last_smtp_transaction_id = false;
0 ignored issues
show
Documentation Bug introduced by
The property $last_smtp_transaction_id was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1255
            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1256
                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1257
                    $this->last_smtp_transaction_id = $matches[1];
1258
                }
1259
            }
1260
        }
1261
1262
        return $this->last_smtp_transaction_id;
1263
    }
1264
1265
    /**
1266
     * Get the queue/transaction ID of the last SMTP transaction
1267
     * If no reply has been received yet, it will return null.
1268
     * If no pattern was matched, it will return false.
1269
     * @return bool|null|string
1270
     * @see recordLastTransactionID()
1271
     */
1272
    public function getLastTransactionID()
1273
    {
1274
        return $this->last_smtp_transaction_id;
1275
    }
1276
}
1277