Completed
Push — master ( b04ab3...703410 )
by Marcus
09:14
created

SMTP::startTLS()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 26
ccs 0
cts 9
cp 0
rs 8.5806
cc 4
eloc 13
nc 5
nop 0
crap 20
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
0 ignored issues
show
Complexity introduced by
This class has 1166 lines of code which exceeds the configured maximum of 1000.

Really long classes often contain too much logic and violate the single responsibility principle.

We suggest to take a look at the “Code” section for options on how to refactor this code.

Loading history...
Complexity introduced by
This class has a complexity of 128 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

You can also find more detailed suggestions for refactoring in the “Code” section of your repository.

Loading history...
28
{
29
    /**
30
     * The PHPMailer SMTP version number.
31
     * @var string
32
     */
33
    const VERSION = '5.2.14';
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.14';
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
     * The socket for the server connection.
155
     * @var resource
156
     */
157
    protected $smtp_conn;
158
159
    /**
160
     * Error information, if any, for the last SMTP command.
161
     * @var array
162
     */
163
    protected $error = array(
164
        'error' => '',
165
        'detail' => '',
166
        'smtp_code' => '',
167
        'smtp_code_ex' => ''
168
    );
169
170
    /**
171
     * The reply the server sent to us for HELO.
172
     * If null, no HELO string has yet been received.
173
     * @var string|null
174
     */
175
    protected $helo_rply = null;
176
177
    /**
178
     * The set of SMTP extensions sent in reply to EHLO command.
179
     * Indexes of the array are extension names.
180
     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
181
     * represents the server name. In case of HELO it is the only element of the array.
182
     * Other values can be boolean TRUE or an array containing extension options.
183
     * If null, no HELO/EHLO string has yet been received.
184
     * @var array|null
185
     */
186
    protected $server_caps = null;
187
188
    /**
189
     * The most recent reply received from the server.
190
     * @var string
191
     */
192
    protected $last_reply = '';
193
194
    /**
195
     * Output debugging info via a user-selected method.
196
     * @see SMTP::$Debugoutput
197
     * @see SMTP::$do_debug
198
     * @param string $str Debug string to output
199
     * @param integer $level The debug level of this message; see DEBUG_* constants
200
     * @return void
201
     */
202 35
    protected function edebug($str, $level = 0)
203
    {
204 35
        if ($level > $this->do_debug) {
205 34
            return;
206
        }
207
        //Avoid clash with built-in function names
208 35
        if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) {
209
            call_user_func($this->Debugoutput, $str, $this->do_debug);
210
            return;
211
        }
212 35
        switch ($this->Debugoutput) {
213 35
            case 'error_log':
214
                //Don't output, just log
215
                error_log($str);
216
                break;
217 35
            case 'html':
218
                //Cleans up output a bit for a better looking, HTML-safe output
219
                echo htmlentities(
220
                    preg_replace('/[\r\n]+/', '', $str),
221
                    ENT_QUOTES,
222
                    'UTF-8'
223
                )
224
                . "<br>\n";
225
                break;
226 35
            case 'echo':
227 35
            default:
228
                //Normalize line breaks
229 35
                $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str);
230 35
                echo gmdate('Y-m-d H:i:s') . "\t" . str_replace(
231 35
                    "\n",
232 35
                    "\n                   \t                  ",
233 35
                    trim($str)
234 35
                )."\n";
235 35
        }
236 35
    }
237
238
    /**
239
     * Connect to an SMTP server.
240
     * @param string $host SMTP server IP or host name
241
     * @param integer $port The port number to connect to
242
     * @param integer $timeout How long to wait for the connection to open
243
     * @param array $options An array of options for stream_context_create()
244
     * @access public
245
     * @return boolean
246
     */
247 35
    public function connect($host, $port = null, $timeout = 30, $options = array())
248
    {
249 35
        static $streamok;
250
        //This is enabled by default since 5.0.0 but some providers disable it
251
        //Check this once and cache the result
252 35
        if (is_null($streamok)) {
253 1
            $streamok = function_exists('stream_socket_client');
254 1
        }
255
        // Clear errors to avoid confusion
256 35
        $this->setError('');
257
        // Make sure we are __not__ connected
258 35
        if ($this->connected()) {
259
            // Already connected, generate error
260
            $this->setError('Already connected to a server');
261
            return false;
262
        }
263 35
        if (empty($port)) {
264
            $port = self::DEFAULT_SMTP_PORT;
265
        }
266
        // Connect to the SMTP server
267 35
        $this->edebug(
268 35
            "Connection: opening to $host:$port, timeout=$timeout, options=".var_export($options, true),
269
            self::DEBUG_CONNECTION
270 35
        );
271 35
        $errno = 0;
272 35
        $errstr = '';
273 35
        if ($streamok) {
274 35
            $socket_context = stream_context_create($options);
275
            //Suppress errors; connection failures are handled at a higher level
276 35
            $this->smtp_conn = @stream_socket_client(
277 35
                $host . ":" . $port,
278
                $errno,
279
                $errstr,
280 35
                $timeout,
281 35
                STREAM_CLIENT_CONNECT,
282
                $socket_context
283 35
            );
284 35
        } else {
285
            //Fall back to fsockopen which should work in more places, but is missing some features
286
            $this->edebug(
287
                "Connection: stream_socket_client not available, falling back to fsockopen",
288
                self::DEBUG_CONNECTION
289
            );
290
            $this->smtp_conn = fsockopen(
291
                $host,
292
                $port,
293
                $errno,
294
                $errstr,
295
                $timeout
296
            );
297
        }
298
        // Verify we connected properly
299 35
        if (!is_resource($this->smtp_conn)) {
300 1
            $this->setError(
301 1
                'Failed to connect to server',
302 1
                $errno,
303
                $errstr
304 1
            );
305 1
            $this->edebug(
306 1
                'SMTP ERROR: ' . $this->error['error']
307 1
                . ": $errstr ($errno)",
308
                self::DEBUG_CLIENT
309 1
            );
310 1
            return false;
311
        }
312 35
        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
313
        // SMTP server can take longer to respond, give longer timeout for first read
314
        // Windows does not have support for this timeout function
315 35
        if (substr(PHP_OS, 0, 3) != 'WIN') {
316 35
            $max = ini_get('max_execution_time');
317
            // Don't bother if unlimited
318 35
            if ($max != 0 && $timeout > $max) {
319
                @set_time_limit($timeout);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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...
320
            }
321 35
            stream_set_timeout($this->smtp_conn, $timeout, 0);
322 35
        }
323
        // Get any announcement
324 35
        $announce = $this->get_lines();
325 35
        $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
326 35
        return true;
327
    }
328
329
    /**
330
     * Initiate a TLS (encrypted) session.
331
     * @access public
332
     * @return boolean
333
     */
334
    public function startTLS()
335
    {
336
        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
337
            return false;
338
        }
339
340
        //Allow the best TLS version(s) we can
341
        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
342
343
        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
344
        //so add them back in manually if we can
345
        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
346
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
347
            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
348
        }
349
350
        // Begin encrypted connection
351
        if (!stream_socket_enable_crypto(
352
            $this->smtp_conn,
353
            true,
354
            $crypto_method
355
        )) {
356
            return false;
357
        }
358
        return true;
359
    }
360
361
    /**
362
     * Perform SMTP authentication.
363
     * Must be run after hello().
364
     * @see hello()
365
     * @param string $username The user name
366
     * @param string $password The password
367
     * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5, XOAUTH2)
368
     * @param string $realm The auth realm for NTLM
369
     * @param string $workstation The auth workstation for NTLM
370
     * @param null|OAuth $OAuth An optional OAuth instance (@see PHPMailerOAuth)
371
     * @return bool True if successfully authenticated.* @access public
372
     */
373
    public function authenticate(
0 ignored issues
show
Complexity introduced by
This operation has 6532 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
374
        $username,
375
        $password,
376
        $authtype = null,
377
        $realm = '',
378
        $workstation = '',
379
        $OAuth = null
380
    ) {
381
        if (!$this->server_caps) {
382
            $this->setError('Authentication is not allowed before HELO/EHLO');
383
            return false;
384
        }
385
386
        if (array_key_exists('EHLO', $this->server_caps)) {
387
        // SMTP extensions are available. Let's try to find a proper authentication method
388
389
            if (!array_key_exists('AUTH', $this->server_caps)) {
390
                $this->setError('Authentication is not allowed at this stage');
391
                // 'at this stage' means that auth may be allowed after the stage changes
392
                // e.g. after STARTTLS
393
                return false;
394
            }
395
396
            self::edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL);
397
            self::edebug(
398
                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
399
                self::DEBUG_LOWLEVEL
400
            );
401
402
            if (empty($authtype)) {
403
                foreach (array('LOGIN', 'CRAM-MD5', 'NTLM', 'PLAIN', 'XOAUTH2') as $method) {
404
                    if (in_array($method, $this->server_caps['AUTH'])) {
405
                        $authtype = $method;
406
                        break;
407
                    }
408
                }
409
                if (empty($authtype)) {
410
                    $this->setError('No supported authentication methods found');
411
                    return false;
412
                }
413
                self::edebug('Auth method selected: '.$authtype, self::DEBUG_LOWLEVEL);
414
            }
415
416
            if (!in_array($authtype, $this->server_caps['AUTH'])) {
417
                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
418
                return false;
419
            }
420
        } elseif (empty($authtype)) {
421
            $authtype = 'LOGIN';
422
        }
423
        switch ($authtype) {
424
            case 'PLAIN':
425
                // Start authentication
426
                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
427
                    return false;
428
                }
429
                // Send encoded username and password
430
                if (!$this->sendCommand(
431
                    'User & Password',
432
                    base64_encode("\0" . $username . "\0" . $password),
433
                    235
434
                )
435
                ) {
436
                    return false;
437
                }
438
                break;
439
            case 'LOGIN':
440
                // Start authentication
441
                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
442
                    return false;
443
                }
444
                if (!$this->sendCommand("Username", base64_encode($username), 334)) {
445
                    return false;
446
                }
447
                if (!$this->sendCommand("Password", base64_encode($password), 235)) {
448
                    return false;
449
                }
450
                break;
451
            case 'XOAUTH2':
452
                //If the OAuth Instance is not set. Can be a case when PHPMailer is used
453
                //instead of PHPMailerOAuth
454
                if (is_null($OAuth)) {
455
                    return false;
456
                }
457
                $oauth = $OAuth->getOauth64();
458
459
                // Start authentication
460
                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
461
                    return false;
462
                }
463
                break;
464
            case 'NTLM':
465
                /*
466
                 * ntlm_sasl_client.php
467
                 * Bundled with Permission
468
                 *
469
                 * How to telnet in windows:
470
                 * http://technet.microsoft.com/en-us/library/aa995718%28EXCHG.65%29.aspx
471
                 * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication
472
                 */
473
                require_once 'extras/ntlm_sasl_client.php';
474
                $temp = new stdClass;
475
                $ntlm_client = new ntlm_sasl_client_class;
476
                //Check that functions are available
477
                if (!$ntlm_client->Initialize($temp)) {
478
                    $this->setError($temp->error);
479
                    $this->edebug(
480
                        'You need to enable some modules in your php.ini file: '
481
                        . $this->error['error'],
482
                        self::DEBUG_CLIENT
483
                    );
484
                    return false;
485
                }
486
                //msg1
487
                $msg1 = $ntlm_client->TypeMsg1($realm, $workstation); //msg1
488
489
                if (!$this->sendCommand(
490
                    'AUTH NTLM',
491
                    'AUTH NTLM ' . base64_encode($msg1),
492
                    334
493
                )
494
                ) {
495
                    return false;
496
                }
497
                //Though 0 based, there is a white space after the 3 digit number
498
                //msg2
499
                $challenge = substr($this->last_reply, 3);
500
                $challenge = base64_decode($challenge);
501
                $ntlm_res = $ntlm_client->NTLMResponse(
502
                    substr($challenge, 24, 8),
503
                    $password
504
                );
505
                //msg3
506
                $msg3 = $ntlm_client->TypeMsg3(
507
                    $ntlm_res,
508
                    $username,
509
                    $realm,
510
                    $workstation
511
                );
512
                // send encoded username
513
                return $this->sendCommand('Username', base64_encode($msg3), 235);
514
            case 'CRAM-MD5':
515
                // Start authentication
516
                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
517
                    return false;
518
                }
519
                // Get the challenge
520
                $challenge = base64_decode(substr($this->last_reply, 4));
521
522
                // Build the response
523
                $response = $username . ' ' . $this->hmac($challenge, $password);
524
525
                // send encoded credentials
526
                return $this->sendCommand('Username', base64_encode($response), 235);
527
            default:
528
                $this->setError("Authentication method \"$authtype\" is not supported");
529
                return false;
530
        }
531
        return true;
532
    }
533
534
    /**
535
     * Calculate an MD5 HMAC hash.
536
     * Works like hash_hmac('md5', $data, $key)
537
     * in case that function is not available
538
     * @param string $data The data to hash
539
     * @param string $key  The key to hash with
540
     * @access protected
541
     * @return string
542
     */
543
    protected function hmac($data, $key)
544
    {
545
        if (function_exists('hash_hmac')) {
546
            return hash_hmac('md5', $data, $key);
547
        }
548
549
        // The following borrowed from
550
        // http://php.net/manual/en/function.mhash.php#27225
551
552
        // RFC 2104 HMAC implementation for php.
553
        // Creates an md5 HMAC.
554
        // Eliminates the need to install mhash to compute a HMAC
555
        // by Lance Rushing
556
557
        $bytelen = 64; // byte length for md5
558
        if (strlen($key) > $bytelen) {
559
            $key = pack('H*', md5($key));
560
        }
561
        $key = str_pad($key, $bytelen, chr(0x00));
562
        $ipad = str_pad('', $bytelen, chr(0x36));
563
        $opad = str_pad('', $bytelen, chr(0x5c));
564 35
        $k_ipad = $key ^ $ipad;
565
        $k_opad = $key ^ $opad;
566 35
567 35
        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
568 35
    }
569
570
    /**
571
     * Check connection state.
572
     * @access public
573
     * @return boolean True if connected.
574
     */
575
    public function connected()
576
    {
577 35
        if (is_resource($this->smtp_conn)) {
578
            $sock_status = stream_get_meta_data($this->smtp_conn);
579 35
            if ($sock_status['eof']) {
580
                // The socket is valid but we are not connected
581
                $this->edebug(
582
                    'SMTP NOTICE: EOF caught while checking if connected',
583
                    self::DEBUG_CLIENT
584
                );
585
                $this->close();
586
                return false;
587
            }
588
            return true; // everything looks good
589 35
        }
590
        return false;
591 35
    }
592 35
593 35
    /**
594 35
     * Close the socket and clean up the state of the class.
595
     * Don't use this function without first trying to use QUIT.
596 35
     * @see quit()
597 35
     * @access public
598 35
     * @return void
599 35
     */
600 35
    public function close()
601
    {
602
        $this->setError('');
603
        $this->server_caps = null;
604
        $this->helo_rply = null;
605
        if (is_resource($this->smtp_conn)) {
606
            // close the connection and cleanup
607
            fclose($this->smtp_conn);
608
            $this->smtp_conn = null; //Makes for cleaner serialization
609
            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
610
        }
611
    }
612
613
    /**
614 33
     * Send an SMTP DATA command.
615
     * Issues a data command and sends the msg_data to the server,
616
     * finializing the mail transaction. $msg_data is the message
617 33
     * that is to be send with the headers. Each header needs to be
618
     * on a single line followed by a <CRLF> with the message headers
619
     * and the message body being separated by and additional <CRLF>.
620
     * Implements rfc 821: DATA <CRLF>
621
     * @param string $msg_data Message data to send
622
     * @access public
623
     * @return boolean
624
     */
625
    public function data($msg_data)
0 ignored issues
show
Complexity introduced by
This operation has 366 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
626
    {
627
        //This will use the standard timelimit
628
        if (!$this->sendCommand('DATA', 'DATA', 354)) {
629
            return false;
630 33
        }
631
632
        /* The server is ready to accept data!
633
         * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF)
634
         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
635
         * smaller lines to fit within the limit.
636
         * We will also look for lines that start with a '.' and prepend an additional '.'.
637 33
         * NOTE: this does not count towards line-length limit.
638 33
         */
639 33
640 33
        // Normalize line breaks before exploding
641 33
        $lines = explode("\n", str_replace(array("\r\n", "\r"), "\n", $msg_data));
642
643 33
        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
644 33
         * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
645 33
         * process all lines before a blank line as headers.
646 33
         */
647 33
648
        $field = substr($lines[0], 0, strpos($lines[0], ':'));
649
        $in_headers = false;
650 33
        if (!empty($field) && strpos($field, ' ') === false) {
651
            $in_headers = true;
652
        }
653 1
654
        foreach ($lines as $line) {
655 1
            $lines_out = array();
656
            if ($in_headers and $line == '') {
657 1
                $in_headers = false;
658 1
            }
659 1
            //Break this line up into several smaller lines if it's too long
660 1
            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
661
            while (isset($line[self::MAX_LINE_LENGTH])) {
662 1
                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
663
                //so as to avoid breaking in the middle of a word
664 1
                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
665
                //Deliberately matches both false and 0
666
                if (!$pos) {
667 1
                    //No nice break found, add a hard break
668 1
                    $pos = self::MAX_LINE_LENGTH - 1;
669 1
                    $lines_out[] = substr($line, 0, $pos);
670 1
                    $line = substr($line, $pos);
671 33
                } else {
672
                    //Break at the found point
673
                    $lines_out[] = substr($line, 0, $pos);
674 33
                    //Move along by the amount we dealt with
675
                    $line = substr($line, $pos + 1);
676 33
                }
677
                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
678
                if ($in_headers) {
679 33
                    $line = "\t" . $line;
680 33
                }
681 33
            }
682
            $lines_out[] = $line;
683
684
            //Send the lines to the server
685 33
            foreach ($lines_out as $line_out) {
686 33
                //RFC2821 section 4.5.2
687 33
                if (!empty($line_out) and $line_out[0] == '.') {
688
                    $line_out = '.' . $line_out;
689 33
                }
690 33
                $this->client_send($line_out . self::CRLF);
691
            }
692
        }
693
694
        //Message data has been sent, complete the command
695
        //Increase timelimit for end of DATA command
696
        $savetimelimit = $this->Timelimit;
697
        $this->Timelimit = $this->Timelimit * 2;
698
        $result = $this->sendCommand('DATA END', '.', 250);
699
        //Restore timelimit
700
        $this->Timelimit = $savetimelimit;
701
        return $result;
702
    }
703 35
704
    /**
705
     * Send an SMTP HELO or EHLO command.
706 35
     * Used to identify the sending server to the receiving server.
707
     * This makes sure that client and server are in a known state.
708
     * Implements RFC 821: HELO <SP> <domain> <CRLF>
709
     * and RFC 2821 EHLO.
710
     * @param string $host The host name or IP to connect to
711
     * @access public
712
     * @return boolean
713
     */
714
    public function hello($host = '')
715
    {
716
        //Try extended hello first (RFC 2821)
717
        return (boolean)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host));
718 35
    }
719
720 35
    /**
721 35
     * Send an SMTP HELO or EHLO command.
722 35
     * Low-level implementation used by hello()
723 35
     * @see hello()
724 35
     * @param string $hello The HELO string
725
     * @param string $host The hostname to say we are
726
     * @access protected
727 35
     * @return boolean
728
     */
729
    protected function sendHello($hello, $host)
730
    {
731
        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
732
        $this->helo_rply = $this->last_reply;
733
        if ($noerror) {
734
            $this->parseHelloFields($hello);
735
        } else {
736 35
            $this->server_caps = null;
737
        }
738 35
        return $noerror;
739 35
    }
740
741 35
    /**
742
     * Parse a reply to HELO/EHLO command to discover server extensions.
743 35
     * In case of HELO, the only parameter that can be discovered is a server name.
744 35
     * @access protected
745 35
     * @param string $type - 'HELO' or 'EHLO'
746
     */
747 35
    protected function parseHelloFields($type)
748 35
    {
749 35
        $this->server_caps = array();
750 35
        $lines = explode("\n", $this->helo_rply);
751 35
752 35
        foreach ($lines as $n => $s) {
753 35
            //First 4 chars contain response code followed by - or space
754
            $s = trim(substr($s, 4));
755 35
            if (empty($s)) {
756
                continue;
757
            }
758 35
            $fields = explode(' ', $s);
759 35
            if (!empty($fields)) {
760
                if (!$n) {
761
                    $name = $type;
762 35
                    $fields = $fields[0];
763 35
                } else {
764 35
                    $name = array_shift($fields);
765 35
                    switch ($name) {
766
                        case 'SIZE':
767 35
                            $fields = ($fields ? $fields[0] : 0);
768 35
                            break;
769 35
                        case 'AUTH':
770 35
                            if (!is_array($fields)) {
771
                                $fields = array();
772
                            }
773
                            break;
774
                        default:
775
                            $fields = true;
776
                    }
777
                }
778
                $this->server_caps[$name] = $fields;
779
            }
780
        }
781
    }
782
783 34
    /**
784
     * Send an SMTP MAIL command.
785 34
     * Starts a mail transaction from the email address specified in
786 34
     * $from. Returns true if successful or false otherwise. If True
787 34
     * the mail transaction is started and then one or more recipient
788 34
     * commands may be called followed by a data command.
789
     * Implements rfc 821: MAIL <SP> FROM:<reverse-path> <CRLF>
790 34
     * @param string $from Source address of this message
791
     * @access public
792
     * @return boolean
793
     */
794
    public function mail($from)
795
    {
796
        $useVerp = ($this->do_verp ? ' XVERP' : '');
797
        return $this->sendCommand(
798
            'MAIL FROM',
799
            'MAIL FROM:<' . $from . '>' . $useVerp,
800
            250
801 35
        );
802
    }
803 35
804 35
    /**
805 35
     * Send an SMTP QUIT command.
806 35
     * Closes the socket if there is no error or the $close_on_error argument is true.
807 35
     * Implements from rfc 821: QUIT <CRLF>
808 35
     * @param boolean $close_on_error Should the connection close if an error occurs?
809 35
     * @access public
810
     * @return boolean
811
     */
812
    public function quit($close_on_error = true)
813
    {
814
        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
815
        $err = $this->error; //Save any error
816
        if ($noerror or $close_on_error) {
817
            $this->close();
818
            $this->error = $err; //Restore any error from the quit command
819
        }
820
        return $noerror;
821 33
    }
822
823 33
    /**
824 33
     * Send an SMTP RCPT command.
825 33
     * Sets the TO argument to $toaddr.
826 33
     * Returns true if the recipient was accepted false if it was rejected.
827 33
     * Implements from rfc 821: RCPT <SP> TO:<forward-path> <CRLF>
828
     * @param string $address The address the message is being sent to
829
     * @access public
830
     * @return boolean
831
     */
832
    public function recipient($address)
833
    {
834
        return $this->sendCommand(
835
            'RCPT TO',
836
            'RCPT TO:<' . $address . '>',
837 1
            array(250, 251)
838
        );
839 1
    }
840
841
    /**
842
     * Send an SMTP RSET command.
843
     * Abort any transaction that is currently in progress.
844
     * Implements rfc 821: RSET <CRLF>
845
     * @access public
846
     * @return boolean True on success.
847
     */
848
    public function reset()
849
    {
850 35
        return $this->sendCommand('RSET', 'RSET', 250);
851
    }
852 35
853
    /**
854
     * Send a command to an SMTP server and check its return code.
855
     * @param string $command The command name - not sent to the server
856
     * @param string $commandstring The actual command to send
857 35
     * @param integer|array $expect One or more expected integer success codes
858 1
     * @access protected
859 1
     * @return boolean True on success.
860
     */
861 35
    protected function sendCommand($command, $commandstring, $expect)
0 ignored issues
show
Complexity introduced by
This operation has 312 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
862
    {
863 35
        if (!$this->connected()) {
864
            $this->setError("Called $command without being connected");
865 35
            return false;
866 35
        }
867 35
        //Reject line breaks in all commands
868 35
        if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) {
869
            $this->setError("Command '$command' contained line breaks");
870 35
            return false;
871 35
        }
872 35
        $this->client_send($commandstring . self::CRLF);
873 35
874 35
        $this->last_reply = $this->get_lines();
875 35
        // Fetch SMTP code and possible error code explanation
876
        $matches = array();
877
        if (preg_match("/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches)) {
878
            $code = $matches[1];
879
            $code_ex = (count($matches) > 2 ? $matches[2] : null);
880
            // Cut off error code from each response line
881
            $detail = preg_replace(
882 35
                "/{$code}[ -]".($code_ex ? str_replace('.', '\\.', $code_ex).' ' : '')."/m",
883
                '',
884 35
                $this->last_reply
885
            );
886
        } else {
887
            // Fall back to simple parsing if regex fails
888
            $code = substr($this->last_reply, 0, 3);
889
            $code_ex = null;
890
            $detail = substr($this->last_reply, 4);
891
        }
892
893
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
894
895
        if (!in_array($code, (array)$expect)) {
896
            $this->setError(
897
                "$command command failed",
898 35
                $detail,
899 35
                $code,
900
                $code_ex
901
            );
902
            $this->edebug(
903
                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
904
                self::DEBUG_CLIENT
905
            );
906
            return false;
907
        }
908
909
        $this->setError('');
910
        return true;
911
    }
912
913
    /**
914
     * Send an SMTP SAML command.
915
     * Starts a mail transaction from the email address specified in $from.
916
     * Returns true if successful or false otherwise. If True
917
     * the mail transaction is started and then one or more recipient
918
     * commands may be called followed by a data command. This command
919
     * will send the message to the users terminal if they are logged
920
     * in and send them an email.
921
     * Implements rfc 821: SAML <SP> FROM:<reverse-path> <CRLF>
922
     * @param string $from The address the message is from
923
     * @access public
924
     * @return boolean
925
     */
926
    public function sendAndMail($from)
927
    {
928
        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
929
    }
930
931
    /**
932
     * Send an SMTP VRFY command.
933
     * @param string $name The name to verify
934
     * @access public
935
     * @return boolean
936
     */
937
    public function verify($name)
938
    {
939
        return $this->sendCommand('VRFY', "VRFY $name", array(250, 251));
940
    }
941
942
    /**
943
     * Send an SMTP NOOP command.
944
     * Used to keep keep-alives alive, doesn't actually do anything
945
     * @access public
946
     * @return boolean
947
     */
948
    public function noop()
949
    {
950
        return $this->sendCommand('NOOP', 'NOOP', 250);
951
    }
952
953
    /**
954
     * Send an SMTP TURN command.
955
     * This is an optional command for SMTP that this class does not support.
956
     * This method is here to make the RFC821 Definition complete for this class
957
     * and _may_ be implemented in future
958
     * Implements from rfc 821: TURN <CRLF>
959
     * @access public
960
     * @return boolean
961
     */
962
    public function turn()
963
    {
964 35
        $this->setError('The SMTP TURN command is not implemented');
965
        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
966 35
        return false;
967 35
    }
968
969
    /**
970
     * Send raw data to the server.
971
     * @param string $data The data to send
972
     * @access public
973
     * @return integer|boolean The number of bytes sent to the server or false on error
974
     */
975 1
    public function client_send($data)
976
    {
977 1
        $this->edebug("CLIENT -> SERVER: $data", self::DEBUG_CLIENT);
978
        return fwrite($this->smtp_conn, $data);
979
    }
980
981
    /**
982
     * Get the latest error.
983
     * @access public
984
     * @return array
985
     */
986
    public function getError()
987
    {
988
        return $this->error;
989
    }
990
991
    /**
992
     * Get SMTP extensions available on the server
993
     * @access public
994
     * @return array|null
995
     */
996
    public function getServerExtList()
997
    {
998
        return $this->server_caps;
999
    }
1000
1001
    /**
1002
     * A multipurpose method
1003
     * The method works in three ways, dependent on argument value and current state
1004
     *   1. HELO/EHLO was not sent - returns null and set up $this->error
1005
     *   2. HELO was sent
1006
     *     $name = 'HELO': returns server name
1007
     *     $name = 'EHLO': returns boolean false
1008
     *     $name = any string: returns null and set up $this->error
1009 35
     *   3. EHLO was sent
1010
     *     $name = 'HELO'|'EHLO': returns server name
1011 35
     *     $name = any string: if extension $name exists, returns boolean True
1012
     *       or its options. Otherwise returns boolean False
1013
     * In other words, one can use this method to detect 3 conditions:
1014
     *  - null returned: handshake was not or we don't know about ext (refer to $this->error)
1015
     *  - false returned: the requested feature exactly not exists
1016
     *  - positive value returned: the requested feature exists
1017 35
     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
1018 35
     * @return mixed
1019
     */
1020
    public function getServerExt($name)
1021 35
    {
1022 35
        if (!$this->server_caps) {
1023
            $this->setError('No HELO/EHLO was sent');
1024
            return null;
1025
        }
1026
1027
        // the tight logic knot ;)
1028
        if (!array_key_exists($name, $this->server_caps)) {
1029
            if ($name == 'HELO') {
1030
                return $this->server_caps['EHLO'];
1031
            }
1032
            if ($name == 'EHLO' || array_key_exists('EHLO', $this->server_caps)) {
1033
                return false;
1034
            }
1035
            $this->setError('HELO handshake was used. Client knows nothing about server extensions');
1036
            return null;
1037
        }
1038
1039
        return $this->server_caps[$name];
1040
    }
1041
1042
    /**
1043
     * Get the last reply from the server.
1044
     * @access public
1045
     * @return string
1046
     */
1047
    public function getLastReply()
1048
    {
1049
        return $this->last_reply;
1050 35
    }
1051
1052
    /**
1053 35
     * Read the SMTP server's response.
1054
     * Either before eof or socket timeout occurs on the operation.
1055
     * With SMTP we can tell if we have more lines to read if the
1056 35
     * 4th character is '-' symbol. If it is a space then we don't
1057 35
     * need to read anything else.
1058 35
     * @access protected
1059 35
     * @return string
1060 35
     */
1061 35
    protected function get_lines()
1062 35
    {
1063 35
        // If the connection is bad, give up straight away
1064 35
        if (!is_resource($this->smtp_conn)) {
1065 35
            return '';
1066 35
        }
1067
        $data = '';
1068 35
        $endtime = 0;
1069 35
        stream_set_timeout($this->smtp_conn, $this->Timeout);
1070
        if ($this->Timelimit > 0) {
1071
            $endtime = time() + $this->Timelimit;
1072 35
        }
1073 35
        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1074
            $str = @fgets($this->smtp_conn, 515);
1075
            $this->edebug("SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL);
1076
            $this->edebug("SMTP -> get_lines(): \$str is  \"$str\"", self::DEBUG_LOWLEVEL);
1077
            $data .= $str;
1078
            // If 4th character is a space, we are done reading, break the loop, micro-optimisation over strlen
1079
            if ((isset($str[3]) and $str[3] == ' ')) {
1080
                break;
1081 35
            }
1082
            // Timed-out? Log and break
1083
            $info = stream_get_meta_data($this->smtp_conn);
1084
            if ($info['timed_out']) {
1085
                $this->edebug(
1086
                    'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)',
1087
                    self::DEBUG_LOWLEVEL
1088
                );
1089 35
                break;
1090 35
            }
1091
            // Now check if reads took too long
1092
            if ($endtime and time() > $endtime) {
1093
                $this->edebug(
1094
                    'SMTP -> get_lines(): timelimit reached ('.
1095
                    $this->Timelimit . ' sec)',
1096
                    self::DEBUG_LOWLEVEL
1097 35
                );
1098
                break;
1099 35
            }
1100 35
        }
1101
        return $data;
1102
    }
1103
1104
    /**
1105
     * Enable or disable VERP address generation.
1106
     * @param boolean $enabled
1107
     */
1108
    public function setVerp($enabled = false)
1109
    {
1110
        $this->do_verp = $enabled;
1111
    }
1112
1113
    /**
1114
     * Get VERP address generation mode.
1115
     * @return boolean
1116
     */
1117
    public function getVerp()
0 ignored issues
show
Coding Style Naming introduced by
The 'getVerp()' method which returns a boolean should be named 'is...()' or 'has...()'
Loading history...
1118 35
    {
1119
        return $this->do_verp;
1120 35
    }
1121 35
1122 35
    /**
1123 35
     * Set error messages and codes.
1124
     * @param string $message The error message
1125 35
     * @param string $detail Further detail on the error
1126 35
     * @param string $smtp_code An associated SMTP error code
1127
     * @param string $smtp_code_ex Extended SMTP code
1128
     */
1129
    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1130
    {
1131
        $this->error = array(
1132 35
            'error' => $message,
1133
            'detail' => $detail,
1134 35
            'smtp_code' => $smtp_code,
1135 35
            'smtp_code_ex' => $smtp_code_ex
1136
        );
1137
    }
1138
1139
    /**
1140
     * Set debug output method.
1141
     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it.
1142
     */
1143
    public function setDebugOutput($method = 'echo')
1144
    {
1145
        $this->Debugoutput = $method;
1146
    }
1147
1148
    /**
1149
     * Get debug output method.
1150 35
     * @return string
1151
     */
1152 35
    public function getDebugOutput()
1153 35
    {
1154
        return $this->Debugoutput;
1155
    }
1156
1157
    /**
1158
     * Set debug output level.
1159
     * @param integer $level
1160
     */
1161
    public function setDebugLevel($level = 0)
1162
    {
1163
        $this->do_debug = $level;
1164
    }
1165
1166
    /**
1167
     * Get debug output level.
1168 35
     * @return integer
1169
     */
1170 35
    public function getDebugLevel()
1171 35
    {
1172
        return $this->do_debug;
1173
    }
1174
1175
    /**
1176
     * Set SMTP timeout.
1177
     * @param integer $timeout
1178
     */
1179
    public function setTimeout($timeout = 0)
1180
    {
1181
        $this->Timeout = $timeout;
1182
    }
1183
1184
    /**
1185
     * Get SMTP timeout.
1186
     * @return integer
1187
     */
1188
    public function getTimeout()
1189
    {
1190
        return $this->Timeout;
1191
    }
1192
}
1193