SMTP::edebug()   B
last analyzed

Complexity

Conditions 8
Paths 7

Size

Total Lines 48

Duplication

Lines 35
Ratio 72.92 %

Importance

Changes 0
Metric Value
cc 8
nc 7
nop 2
dl 35
loc 48
rs 7.8901
c 0
b 0
f 0
1
<?php
2
/**
3
 * PHPMailer RFC821 SMTP email transport class.
4
 * PHP Version 5.5.
5
 *
6
 * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
7
 *
8
 * @author    Marcus Bointon (Synchro/coolbru) <[email protected]>
9
 * @author    Jim Jagielski (jimjag) <[email protected]>
10
 * @author    Andy Prevost (codeworxtech) <[email protected]>
11
 * @author    Brent R. Matzelle (original founder)
12
 * @copyright 2012 - 2017 Marcus Bointon
13
 * @copyright 2010 - 2012 Jim Jagielski
14
 * @copyright 2004 - 2009 Andy Prevost
15
 * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
16
 * @note      This program is distributed in the hope that it will be useful - WITHOUT
17
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18
 * FITNESS FOR A PARTICULAR PURPOSE.
19
 */
20
21
namespace PHPMailer\PHPMailer;
22
23
/**
24
 * PHPMailer RFC821 SMTP email transport class.
25
 * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
26
 *
27
 * @author  Chris Ryan
28
 * @author  Marcus Bointon <[email protected]>
29
 */
30
class SMTP
31
{
32
    /**
33
     * The PHPMailer SMTP version number.
34
     *
35
     * @var string
36
     */
37
    const VERSION = '6.0.5';
38
39
    /**
40
     * SMTP line break constant.
41
     *
42
     * @var string
43
     */
44
    const LE = "\r\n";
45
46
    /**
47
     * The SMTP port to use if one is not specified.
48
     *
49
     * @var int
50
     */
51
    const DEFAULT_PORT = 25;
52
53
    /**
54
     * The maximum line length allowed by RFC 2822 section 2.1.1.
55
     *
56
     * @var int
57
     */
58
    const MAX_LINE_LENGTH = 998;
59
60
    /**
61
     * Debug level for no output.
62
     */
63
    const DEBUG_OFF = 0;
64
65
    /**
66
     * Debug level to show client -> server messages.
67
     */
68
    const DEBUG_CLIENT = 1;
69
70
    /**
71
     * Debug level to show client -> server and server -> client messages.
72
     */
73
    const DEBUG_SERVER = 2;
74
75
    /**
76
     * Debug level to show connection status, client -> server and server -> client messages.
77
     */
78
    const DEBUG_CONNECTION = 3;
79
80
    /**
81
     * Debug level to show all messages.
82
     */
83
    const DEBUG_LOWLEVEL = 4;
84
85
    /**
86
     * Debug output level.
87
     * Options:
88
     * * self::DEBUG_OFF (`0`) No debug output, default
89
     * * self::DEBUG_CLIENT (`1`) Client commands
90
     * * self::DEBUG_SERVER (`2`) Client commands and server responses
91
     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
92
     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
93
     *
94
     * @var int
95
     */
96
    public $do_debug = self::DEBUG_OFF;
97
98
    /**
99
     * How to handle debug output.
100
     * Options:
101
     * * `echo` Output plain-text as-is, appropriate for CLI
102
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
103
     * * `error_log` Output to error log as configured in php.ini
104
     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
105
     *
106
     * ```php
107
     * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
108
     * ```
109
     *
110
     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
111
     * level output is used:
112
     *
113
     * ```php
114
     * $mail->Debugoutput = new myPsr3Logger;
115
     * ```
116
     *
117
     * @var string|callable|\Psr\Log\LoggerInterface
118
     */
119
    public $Debugoutput = 'echo';
120
121
    /**
122
     * Whether to use VERP.
123
     *
124
     * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
125
     * @see http://www.postfix.org/VERP_README.html Info on VERP
126
     *
127
     * @var bool
128
     */
129
    public $do_verp = false;
130
131
    /**
132
     * The timeout value for connection, in seconds.
133
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
134
     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
135
     *
136
     * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
137
     *
138
     * @var int
139
     */
140
    public $Timeout = 300;
141
142
    /**
143
     * How long to wait for commands to complete, in seconds.
144
     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
145
     *
146
     * @var int
147
     */
148
    public $Timelimit = 300;
149
150
    /**
151
     * Patterns to extract an SMTP transaction id from reply to a DATA command.
152
     * The first capture group in each regex will be used as the ID.
153
     * MS ESMTP returns the message ID, which may not be correct for internal tracking.
154
     *
155
     * @var string[]
156
     */
157
    protected $smtp_transaction_id_patterns = [
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
        'Microsoft_ESMTP' => '/[0-9]{3} 2.[0-9].0 (.*)@(?:.*) Queued mail for delivery/',
162
        'Amazon_SES' => '/[0-9]{3} Ok (.*)/',
163
        'SendGrid' => '/[0-9]{3} Ok: queued as (.*)/',
164
    ];
165
166
    /**
167
     * The last transaction ID issued in response to a DATA command,
168
     * if one was detected.
169
     *
170
     * @var string|bool|null
171
     */
172
    protected $last_smtp_transaction_id;
173
174
    /**
175
     * The socket for the server connection.
176
     *
177
     * @var ?resource
178
     */
179
    protected $smtp_conn;
180
181
    /**
182
     * Error information, if any, for the last SMTP command.
183
     *
184
     * @var array
185
     */
186
    protected $error = [
187
        'error' => '',
188
        'detail' => '',
189
        'smtp_code' => '',
190
        'smtp_code_ex' => '',
191
    ];
192
193
    /**
194
     * The reply the server sent to us for HELO.
195
     * If null, no HELO string has yet been received.
196
     *
197
     * @var string|null
198
     */
199
    protected $helo_rply = null;
200
201
    /**
202
     * The set of SMTP extensions sent in reply to EHLO command.
203
     * Indexes of the array are extension names.
204
     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
205
     * represents the server name. In case of HELO it is the only element of the array.
206
     * Other values can be boolean TRUE or an array containing extension options.
207
     * If null, no HELO/EHLO string has yet been received.
208
     *
209
     * @var array|null
210
     */
211
    protected $server_caps = null;
212
213
    /**
214
     * The most recent reply received from the server.
215
     *
216
     * @var string
217
     */
218
    protected $last_reply = '';
219
220
    /**
221
     * Output debugging info via a user-selected method.
222
     *
223
     * @param string $str   Debug string to output
224
     * @param int    $level The debug level of this message; see DEBUG_* constants
225
     *
226
     * @see SMTP::$Debugoutput
227
     * @see SMTP::$do_debug
228
     */
229
    protected function edebug($str, $level = 0)
230
    {
231
        if ($level > $this->do_debug) {
232
            return;
233
        }
234
        //Is this a PSR-3 logger?
235
        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
236
            $this->Debugoutput->debug($str);
237
238
            return;
239
        }
240
        //Avoid clash with built-in function names
241 View Code Duplication
        if (!in_array($this->Debugoutput, ['error_log', 'html', 'echo']) and is_callable($this->Debugoutput)) {
242
            call_user_func($this->Debugoutput, $str, $level);
243
244
            return;
245
        }
246 View Code Duplication
        switch ($this->Debugoutput) {
247
            case 'error_log':
248
                //Don't output, just log
249
                error_log($str);
250
                break;
251
            case 'html':
252
                //Cleans up output a bit for a better looking, HTML-safe output
253
                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
254
                    preg_replace('/[\r\n]+/', '', $str),
255
                    ENT_QUOTES,
256
                    'UTF-8'
257
                ), "<br>\n";
258
                break;
259
            case 'echo':
260
            default:
261
                //Normalize line breaks
262
                $str = preg_replace('/\r\n|\r/ms', "\n", $str);
263
                echo gmdate('Y-m-d H:i:s'),
264
                "\t",
265
                    //Trim trailing space
266
                trim(
267
                //Indent for readability, except for trailing break
268
                    str_replace(
269
                        "\n",
270
                        "\n                   \t                  ",
271
                        trim($str)
272
                    )
273
                ),
274
                "\n";
275
        }
276
    }
277
278
    /**
279
     * Connect to an SMTP server.
280
     *
281
     * @param string $host    SMTP server IP or host name
282
     * @param int    $port    The port number to connect to
0 ignored issues
show
Documentation introduced by
Should the type for parameter $port not be integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
283
     * @param int    $timeout How long to wait for the connection to open
284
     * @param array  $options An array of options for stream_context_create()
285
     *
286
     * @return bool
287
     */
288
    public function connect($host, $port = null, $timeout = 30, $options = [])
289
    {
290
        static $streamok;
291
        //This is enabled by default since 5.0.0 but some providers disable it
292
        //Check this once and cache the result
293
        if (null === $streamok) {
294
            $streamok = function_exists('stream_socket_client');
295
        }
296
        // Clear errors to avoid confusion
297
        $this->setError('');
298
        // Make sure we are __not__ connected
299
        if ($this->connected()) {
300
            // Already connected, generate error
301
            $this->setError('Already connected to a server');
302
303
            return false;
304
        }
305
        if (empty($port)) {
306
            $port = self::DEFAULT_PORT;
307
        }
308
        // Connect to the SMTP server
309
        $this->edebug(
310
            "Connection: opening to $host:$port, timeout=$timeout, options=" .
311
            (count($options) > 0 ? var_export($options, true) : 'array()'),
312
            self::DEBUG_CONNECTION
313
        );
314
        $errno = 0;
315
        $errstr = '';
316
        if ($streamok) {
317
            $socket_context = stream_context_create($options);
318
            set_error_handler([$this, 'errorHandler']);
319
            $this->smtp_conn = stream_socket_client(
320
                $host . ':' . $port,
321
                $errno,
322
                $errstr,
323
                $timeout,
324
                STREAM_CLIENT_CONNECT,
325
                $socket_context
326
            );
327
            restore_error_handler();
328
        } else {
329
            //Fall back to fsockopen which should work in more places, but is missing some features
330
            $this->edebug(
331
                'Connection: stream_socket_client not available, falling back to fsockopen',
332
                self::DEBUG_CONNECTION
333
            );
334
            set_error_handler([$this, 'errorHandler']);
335
            $this->smtp_conn = fsockopen(
336
                $host,
337
                $port,
338
                $errno,
339
                $errstr,
340
                $timeout
341
            );
342
            restore_error_handler();
343
        }
344
        // Verify we connected properly
345
        if (!is_resource($this->smtp_conn)) {
346
            $this->setError(
347
                'Failed to connect to server',
348
                '',
349
                (string) $errno,
350
                (string) $errstr
351
            );
352
            $this->edebug(
353
                'SMTP ERROR: ' . $this->error['error']
354
                . ": $errstr ($errno)",
355
                self::DEBUG_CLIENT
356
            );
357
358
            return false;
359
        }
360
        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
361
        // SMTP server can take longer to respond, give longer timeout for first read
362
        // Windows does not have support for this timeout function
363
        if (substr(PHP_OS, 0, 3) != 'WIN') {
364
            $max = ini_get('max_execution_time');
365
            // Don't bother if unlimited
366
            if (0 != $max and $timeout > $max) {
367
                @set_time_limit($timeout);
368
            }
369
            stream_set_timeout($this->smtp_conn, $timeout, 0);
370
        }
371
        // Get any announcement
372
        $announce = $this->get_lines();
373
        $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
374
375
        return true;
376
    }
377
378
    /**
379
     * Initiate a TLS (encrypted) session.
380
     *
381
     * @return bool
382
     */
383
    public function startTLS()
384
    {
385
        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
386
            return false;
387
        }
388
389
        //Allow the best TLS version(s) we can
390
        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
391
392
        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
393
        //so add them back in manually if we can
394
        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
395
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
396
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
397
        }
398
399
        // Begin encrypted connection
400
        set_error_handler([$this, 'errorHandler']);
401
        $crypto_ok = stream_socket_enable_crypto(
402
            $this->smtp_conn,
403
            true,
404
            $crypto_method
405
        );
406
        restore_error_handler();
407
408
        return (bool) $crypto_ok;
409
    }
410
411
    /**
412
     * Perform SMTP authentication.
413
     * Must be run after hello().
414
     *
415
     * @see    hello()
416
     *
417
     * @param string $username The user name
418
     * @param string $password The password
419
     * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $authtype not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
420
     * @param OAuth  $OAuth    An optional OAuth instance for XOAUTH2 authentication
0 ignored issues
show
Documentation introduced by
Should the type for parameter $OAuth not be OAuth|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

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

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
1216
     */
1217
    public function getDebugOutput()
1218
    {
1219
        return $this->Debugoutput;
1220
    }
1221
1222
    /**
1223
     * Set debug output level.
1224
     *
1225
     * @param int $level
1226
     */
1227
    public function setDebugLevel($level = 0)
1228
    {
1229
        $this->do_debug = $level;
1230
    }
1231
1232
    /**
1233
     * Get debug output level.
1234
     *
1235
     * @return int
1236
     */
1237
    public function getDebugLevel()
1238
    {
1239
        return $this->do_debug;
1240
    }
1241
1242
    /**
1243
     * Set SMTP timeout.
1244
     *
1245
     * @param int $timeout The timeout duration in seconds
1246
     */
1247
    public function setTimeout($timeout = 0)
1248
    {
1249
        $this->Timeout = $timeout;
1250
    }
1251
1252
    /**
1253
     * Get SMTP timeout.
1254
     *
1255
     * @return int
1256
     */
1257
    public function getTimeout()
1258
    {
1259
        return $this->Timeout;
1260
    }
1261
1262
    /**
1263
     * Reports an error number and string.
1264
     *
1265
     * @param int    $errno   The error number returned by PHP
1266
     * @param string $errmsg  The error message returned by PHP
1267
     * @param string $errfile The file the error occurred in
1268
     * @param int    $errline The line number the error occurred on
1269
     */
1270
    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1271
    {
1272
        $notice = 'Connection failed.';
1273
        $this->setError(
1274
            $notice,
1275
            $errmsg,
1276
            (string) $errno
1277
        );
1278
        $this->edebug(
1279
            "$notice Error #$errno: $errmsg [$errfile line $errline]",
1280
            self::DEBUG_CONNECTION
1281
        );
1282
    }
1283
1284
    /**
1285
     * Extract and return the ID of the last SMTP transaction based on
1286
     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1287
     * Relies on the host providing the ID in response to a DATA command.
1288
     * If no reply has been received yet, it will return null.
1289
     * If no pattern was matched, it will return false.
1290
     *
1291
     * @return bool|null|string
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string|false|null.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1292
     */
1293
    protected function recordLastTransactionID()
1294
    {
1295
        $reply = $this->getLastReply();
1296
1297
        if (empty($reply)) {
1298
            $this->last_smtp_transaction_id = null;
1299
        } else {
1300
            $this->last_smtp_transaction_id = false;
1301
            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1302
                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1303
                    $this->last_smtp_transaction_id = trim($matches[1]);
1304
                    break;
1305
                }
1306
            }
1307
        }
1308
1309
        return $this->last_smtp_transaction_id;
1310
    }
1311
1312
    /**
1313
     * Get the queue/transaction ID of the last SMTP transaction
1314
     * If no reply has been received yet, it will return null.
1315
     * If no pattern was matched, it will return false.
1316
     *
1317
     * @return bool|null|string
1318
     *
1319
     * @see recordLastTransactionID()
1320
     */
1321
    public function getLastTransactionID()
1322
    {
1323
        return $this->last_smtp_transaction_id;
1324
    }
1325
}
1326