GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

SMTP::authenticate()   F
last analyzed

Complexity

Conditions 23
Paths 89

Size

Total Lines 146

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
nc 89
nop 5
dl 0
loc 146
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * PHPMailer RFC821 SMTP email transport class.
4
 * PHP Version 5
5
 * @package PHPMailer
6
 * @link https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
7
 * @author Marcus Bointon (Synchro/coolbru) <[email protected]>
8
 * @author Jim Jagielski (jimjag) <[email protected]>
9
 * @author Andy Prevost (codeworxtech) <[email protected]>
10
 * @author Brent R. Matzelle (original founder)
11
 * @copyright 2014 Marcus Bointon
12
 * @copyright 2010 - 2012 Jim Jagielski
13
 * @copyright 2004 - 2009 Andy Prevost
14
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
15
 * @note This program is distributed in the hope that it will be useful - WITHOUT
16
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17
 * FITNESS FOR A PARTICULAR PURPOSE.
18
 */
19
20
/**
21
 * PHPMailer RFC821 SMTP email transport class.
22
 * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
23
 * @package PHPMailer
24
 * @author Chris Ryan
25
 * @author Marcus Bointon <[email protected]>
26
 */
27
class SMTP
28
{
29
    /**
30
     * The PHPMailer SMTP version number.
31
     * @type string
32
     */
33
    const VERSION = '5.2.10';
34
35
    /**
36
     * SMTP line break constant.
37
     * @type string
38
     */
39
    const CRLF = "\r\n";
40
41
    /**
42
     * The SMTP port to use if one is not specified.
43
     * @type integer
44
     */
45
    const DEFAULT_SMTP_PORT = 25;
46
47
    /**
48
     * The maximum line length allowed by RFC 2822 section 2.1.1
49
     * @type 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
     * @type string
81
     * @deprecated Use the `VERSION` constant instead
82
     * @see SMTP::VERSION
83
     */
84
    public $Version = '5.2.10';
85
86
    /**
87
     * SMTP server port number.
88
     * @type 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
     * @type 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
     * @type 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
     * @type 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
     * @type 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
     * @type 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
     * @type integer
150
     */
151
    public $Timelimit = 300;
152
153
    /**
154
     * The socket for the server connection.
155
     * @type resource
156
     */
157
    protected $smtp_conn;
158
159
    /**
160
     * Error information, if any, for the last SMTP command.
161
     * @type 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
     * @type 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
     * @type array|null
185
     */
186
    protected $server_caps = null;
187
188
    /**
189
     * The most recent reply received from the server.
190
     * @type 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 View Code Duplication
    protected function edebug($str, $level = 0)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
203
    {
204
        if ($level > $this->do_debug) {
205
            return;
206
        }
207
        //Avoid clash with built-in function names
208
        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
        switch ($this->Debugoutput) {
213
            case 'error_log':
214
                //Don't output, just log
215
                error_log($str);
216
                break;
217
            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
            case 'echo':
227
            default:
228
                //Normalize line breaks
229
                $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str);
230
                echo gmdate('Y-m-d H:i:s') . "\t" . str_replace(
231
                    "\n",
232
                    "\n                   \t                  ",
233
                    trim($str)
234
                )."\n";
235
        }
236
    }
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
    public function connect($host, $port = null, $timeout = 30, $options = array())
248
    {
249
        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
        if (is_null($streamok)) {
253
            $streamok = function_exists('stream_socket_client');
254
        }
255
        // Clear errors to avoid confusion
256
        $this->setError('');
257
        // Make sure we are __not__ connected
258
        if ($this->connected()) {
259
            // Already connected, generate error
260
            $this->setError('Already connected to a server');
261
            return false;
262
        }
263
        if (empty($port)) {
264
            $port = self::DEFAULT_SMTP_PORT;
265
        }
266
        // Connect to the SMTP server
267
        $this->edebug(
268
            "Connection: opening to $host:$port, timeout=$timeout, options=".var_export($options, true),
269
            self::DEBUG_CONNECTION
270
        );
271
        $errno = 0;
272
        $errstr = '';
273
        if ($streamok) {
274
            $socket_context = stream_context_create($options);
275
            //Suppress errors; connection failures are handled at a higher level
276
            $this->smtp_conn = @stream_socket_client(
277
                $host . ":" . $port,
278
                $errno,
279
                $errstr,
280
                $timeout,
281
                STREAM_CLIENT_CONNECT,
282
                $socket_context
283
            );
284
        } 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
        if (!is_resource($this->smtp_conn)) {
300
            $this->setError(
301
                'Failed to connect to server',
302
                $errno,
303
                $errstr
304
            );
305
            $this->edebug(
306
                'SMTP ERROR: ' . $this->error['error']
307
                . ": $errstr ($errno)",
308
                self::DEBUG_CLIENT
309
            );
310
            return false;
311
        }
312
        $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
        if (substr(PHP_OS, 0, 3) != 'WIN') {
316
            $max = ini_get('max_execution_time');
317
            // Don't bother if unlimited
318
            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
            stream_set_timeout($this->smtp_conn, $timeout, 0);
322
        }
323
        // Get any announcement
324
        $announce = $this->get_lines();
325
        $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
326
        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
        // Begin encrypted connection
340
        if (!stream_socket_enable_crypto(
341
            $this->smtp_conn,
342
            true,
343
            STREAM_CRYPTO_METHOD_TLS_CLIENT
344
        )) {
345
            return false;
346
        }
347
        return true;
348
    }
349
350
    /**
351
     * Perform SMTP authentication.
352
     * Must be run after hello().
353
     * @see hello()
354
     * @param string $username    The user name
355
     * @param string $password    The password
356
     * @param string $authtype    The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5)
357
     * @param string $realm       The auth realm for NTLM
358
     * @param string $workstation The auth workstation for NTLM
359
     * @access public
360
     * @return boolean True if successfully authenticated.
361
     */
362
    public function authenticate(
363
        $username,
364
        $password,
365
        $authtype = null,
366
        $realm = '',
367
        $workstation = ''
368
    ) {
369
        if (!$this->server_caps) {
370
            $this->setError('Authentication is not allowed before HELO/EHLO');
371
            return false;
372
        }
373
374
        if (array_key_exists('EHLO', $this->server_caps)) {
375
        // SMTP extensions are available. Let's try to find a proper authentication method
376
377
            if (!array_key_exists('AUTH', $this->server_caps)) {
378
                $this->setError('Authentication is not allowed at this stage');
379
                // 'at this stage' means that auth may be allowed after the stage changes
380
                // e.g. after STARTTLS
381
                return false;
382
            }
383
384
            self::edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL);
385
            self::edebug(
386
                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
387
                self::DEBUG_LOWLEVEL
388
            );
389
390
            if (empty($authtype)) {
391
                foreach (array('LOGIN', 'CRAM-MD5', 'NTLM', 'PLAIN') as $method) {
392
                    if (in_array($method, $this->server_caps['AUTH'])) {
393
                        $authtype = $method;
394
                        break;
395
                    }
396
                }
397
                if (empty($authtype)) {
398
                    $this->setError('No supported authentication methods found');
399
                    return false;
400
                }
401
                self::edebug('Auth method selected: '.$authtype, self::DEBUG_LOWLEVEL);
402
            }
403
404
            if (!in_array($authtype, $this->server_caps['AUTH'])) {
405
                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
406
                return false;
407
            }
408
        } elseif (empty($authtype)) {
409
            $authtype = 'LOGIN';
410
        }
411
        switch ($authtype) {
412
            case 'PLAIN':
413
                // Start authentication
414
                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
415
                    return false;
416
                }
417
                // Send encoded username and password
418
                if (!$this->sendCommand(
419
                    'User & Password',
420
                    base64_encode("\0" . $username . "\0" . $password),
421
                    235
422
                )
423
                ) {
424
                    return false;
425
                }
426
                break;
427
            case 'LOGIN':
428
                // Start authentication
429
                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
430
                    return false;
431
                }
432
                if (!$this->sendCommand("Username", base64_encode($username), 334)) {
433
                    return false;
434
                }
435
                if (!$this->sendCommand("Password", base64_encode($password), 235)) {
436
                    return false;
437
                }
438
                break;
439
            case 'NTLM':
440
                /*
441
                 * ntlm_sasl_client.php
442
                 * Bundled with Permission
443
                 *
444
                 * How to telnet in windows:
445
                 * http://technet.microsoft.com/en-us/library/aa995718%28EXCHG.65%29.aspx
446
                 * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication
447
                 */
448
                require_once 'extras/ntlm_sasl_client.php';
449
                $temp = new stdClass;
450
                $ntlm_client = new ntlm_sasl_client_class;
451
                //Check that functions are available
452
                if (!$ntlm_client->Initialize($temp)) {
453
                    $this->setError($temp->error);
454
                    $this->edebug(
455
                        'You need to enable some modules in your php.ini file: '
456
                        . $this->error['error'],
457
                        self::DEBUG_CLIENT
458
                    );
459
                    return false;
460
                }
461
                //msg1
462
                $msg1 = $ntlm_client->TypeMsg1($realm, $workstation); //msg1
463
464
                if (!$this->sendCommand(
465
                    'AUTH NTLM',
466
                    'AUTH NTLM ' . base64_encode($msg1),
467
                    334
468
                )
469
                ) {
470
                    return false;
471
                }
472
                //Though 0 based, there is a white space after the 3 digit number
473
                //msg2
474
                $challenge = substr($this->last_reply, 3);
475
                $challenge = base64_decode($challenge);
476
                $ntlm_res = $ntlm_client->NTLMResponse(
477
                    substr($challenge, 24, 8),
478
                    $password
479
                );
480
                //msg3
481
                $msg3 = $ntlm_client->TypeMsg3(
482
                    $ntlm_res,
483
                    $username,
484
                    $realm,
485
                    $workstation
486
                );
487
                // send encoded username
488
                return $this->sendCommand('Username', base64_encode($msg3), 235);
489
            case 'CRAM-MD5':
490
                // Start authentication
491
                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
492
                    return false;
493
                }
494
                // Get the challenge
495
                $challenge = base64_decode(substr($this->last_reply, 4));
496
497
                // Build the response
498
                $response = $username . ' ' . $this->hmac($challenge, $password);
499
500
                // send encoded credentials
501
                return $this->sendCommand('Username', base64_encode($response), 235);
502
            default:
503
                $this->setError("Authentication method \"$authtype\" is not supported");
504
                return false;
505
        }
506
        return true;
507
    }
508
509
    /**
510
     * Calculate an MD5 HMAC hash.
511
     * Works like hash_hmac('md5', $data, $key)
512
     * in case that function is not available
513
     * @param string $data The data to hash
514
     * @param string $key  The key to hash with
515
     * @access protected
516
     * @return string
517
     */
518
    protected function hmac($data, $key)
519
    {
520
        if (function_exists('hash_hmac')) {
521
            return hash_hmac('md5', $data, $key);
522
        }
523
524
        // The following borrowed from
525
        // http://php.net/manual/en/function.mhash.php#27225
526
527
        // RFC 2104 HMAC implementation for php.
528
        // Creates an md5 HMAC.
529
        // Eliminates the need to install mhash to compute a HMAC
530
        // by Lance Rushing
531
532
        $bytelen = 64; // byte length for md5
533
        if (strlen($key) > $bytelen) {
534
            $key = pack('H*', md5($key));
535
        }
536
        $key = str_pad($key, $bytelen, chr(0x00));
537
        $ipad = str_pad('', $bytelen, chr(0x36));
538
        $opad = str_pad('', $bytelen, chr(0x5c));
539
        $k_ipad = $key ^ $ipad;
540
        $k_opad = $key ^ $opad;
541
542
        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
543
    }
544
545
    /**
546
     * Check connection state.
547
     * @access public
548
     * @return boolean True if connected.
549
     */
550
    public function connected()
551
    {
552
        if (is_resource($this->smtp_conn)) {
553
            $sock_status = stream_get_meta_data($this->smtp_conn);
554
            if ($sock_status['eof']) {
555
                // The socket is valid but we are not connected
556
                $this->edebug(
557
                    'SMTP NOTICE: EOF caught while checking if connected',
558
                    self::DEBUG_CLIENT
559
                );
560
                $this->close();
561
                return false;
562
            }
563
            return true; // everything looks good
564
        }
565
        return false;
566
    }
567
568
    /**
569
     * Close the socket and clean up the state of the class.
570
     * Don't use this function without first trying to use QUIT.
571
     * @see quit()
572
     * @access public
573
     * @return void
574
     */
575
    public function close()
576
    {
577
        $this->setError('');
578
        $this->server_caps = null;
579
        $this->helo_rply = null;
580
        if (is_resource($this->smtp_conn)) {
581
            // close the connection and cleanup
582
            fclose($this->smtp_conn);
583
            $this->smtp_conn = null; //Makes for cleaner serialization
584
            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
585
        }
586
    }
587
588
    /**
589
     * Send an SMTP DATA command.
590
     * Issues a data command and sends the msg_data to the server,
591
     * finializing the mail transaction. $msg_data is the message
592
     * that is to be send with the headers. Each header needs to be
593
     * on a single line followed by a <CRLF> with the message headers
594
     * and the message body being separated by and additional <CRLF>.
595
     * Implements rfc 821: DATA <CRLF>
596
     * @param string $msg_data Message data to send
597
     * @access public
598
     * @return boolean
599
     */
600
    public function data($msg_data)
601
    {
602
        //This will use the standard timelimit
603
        if (!$this->sendCommand('DATA', 'DATA', 354)) {
604
            return false;
605
        }
606
607
        /* The server is ready to accept data!
608
         * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF)
609
         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
610
         * smaller lines to fit within the limit.
611
         * We will also look for lines that start with a '.' and prepend an additional '.'.
612
         * NOTE: this does not count towards line-length limit.
613
         */
614
615
        // Normalize line breaks before exploding
616
        $lines = explode("\n", str_replace(array("\r\n", "\r"), "\n", $msg_data));
617
618
        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
619
         * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
620
         * process all lines before a blank line as headers.
621
         */
622
623
        $field = substr($lines[0], 0, strpos($lines[0], ':'));
624
        $in_headers = false;
625
        if (!empty($field) && strpos($field, ' ') === false) {
626
            $in_headers = true;
627
        }
628
629
        foreach ($lines as $line) {
630
            $lines_out = array();
631
            if ($in_headers and $line == '') {
632
                $in_headers = false;
633
            }
634
            //Break this line up into several smaller lines if it's too long
635
            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
636
            while (isset($line[self::MAX_LINE_LENGTH])) {
637
                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
638
                //so as to avoid breaking in the middle of a word
639
                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
640
                //Deliberately matches both false and 0
641
                if (!$pos) {
642
                    //No nice break found, add a hard break
643
                    $pos = self::MAX_LINE_LENGTH - 1;
644
                    $lines_out[] = substr($line, 0, $pos);
645
                    $line = substr($line, $pos);
646
                } else {
647
                    //Break at the found point
648
                    $lines_out[] = substr($line, 0, $pos);
649
                    //Move along by the amount we dealt with
650
                    $line = substr($line, $pos + 1);
651
                }
652
                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
653
                if ($in_headers) {
654
                    $line = "\t" . $line;
655
                }
656
            }
657
            $lines_out[] = $line;
658
659
            //Send the lines to the server
660
            foreach ($lines_out as $line_out) {
661
                //RFC2821 section 4.5.2
662
                if (!empty($line_out) and $line_out[0] == '.') {
663
                    $line_out = '.' . $line_out;
664
                }
665
                $this->client_send($line_out . self::CRLF);
666
            }
667
        }
668
669
        //Message data has been sent, complete the command
670
        //Increase timelimit for end of DATA command
671
        $savetimelimit = $this->Timelimit;
672
        $this->Timelimit = $this->Timelimit * 2;
673
        $result = $this->sendCommand('DATA END', '.', 250);
674
        //Restore timelimit
675
        $this->Timelimit = $savetimelimit;
676
        return $result;
677
    }
678
679
    /**
680
     * Send an SMTP HELO or EHLO command.
681
     * Used to identify the sending server to the receiving server.
682
     * This makes sure that client and server are in a known state.
683
     * Implements RFC 821: HELO <SP> <domain> <CRLF>
684
     * and RFC 2821 EHLO.
685
     * @param string $host The host name or IP to connect to
686
     * @access public
687
     * @return boolean
688
     */
689
    public function hello($host = '')
690
    {
691
        //Try extended hello first (RFC 2821)
692
        return (boolean)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host));
693
    }
694
695
    /**
696
     * Send an SMTP HELO or EHLO command.
697
     * Low-level implementation used by hello()
698
     * @see hello()
699
     * @param string $hello The HELO string
700
     * @param string $host The hostname to say we are
701
     * @access protected
702
     * @return boolean
703
     */
704
    protected function sendHello($hello, $host)
705
    {
706
        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
707
        $this->helo_rply = $this->last_reply;
708
        if ($noerror) {
709
            $this->parseHelloFields($hello);
710
        } else {
711
            $this->server_caps = null;
712
        }
713
        return $noerror;
714
    }
715
716
    /**
717
     * Parse a reply to HELO/EHLO command to discover server extensions.
718
     * In case of HELO, the only parameter that can be discovered is a server name.
719
     * @access protected
720
     * @param string $type - 'HELO' or 'EHLO'
721
     */
722
    protected function parseHelloFields($type)
723
    {
724
        $this->server_caps = array();
725
        $lines = explode("\n", $this->last_reply);
726
        foreach ($lines as $n => $s) {
727
            $s = trim(substr($s, 4));
728
            if (!$s) {
729
                continue;
730
            }
731
            $fields = explode(' ', $s);
732
            if (!empty($fields)) {
733
                if (!$n) {
734
                    $name = $type;
735
                    $fields = $fields[0];
736
                } else {
737
                    $name = array_shift($fields);
738
                    if ($name == 'SIZE') {
739
                        $fields = ($fields) ? $fields[0] : 0;
740
                    }
741
                }
742
                $this->server_caps[$name] = ($fields ? $fields : true);
743
            }
744
        }
745
    }
746
747
    /**
748
     * Send an SMTP MAIL command.
749
     * Starts a mail transaction from the email address specified in
750
     * $from. Returns true if successful or false otherwise. If True
751
     * the mail transaction is started and then one or more recipient
752
     * commands may be called followed by a data command.
753
     * Implements rfc 821: MAIL <SP> FROM:<reverse-path> <CRLF>
754
     * @param string $from Source address of this message
755
     * @access public
756
     * @return boolean
757
     */
758
    public function mail($from)
759
    {
760
        $useVerp = ($this->do_verp ? ' XVERP' : '');
761
        return $this->sendCommand(
762
            'MAIL FROM',
763
            'MAIL FROM:<' . $from . '>' . $useVerp,
764
            250
765
        );
766
    }
767
768
    /**
769
     * Send an SMTP QUIT command.
770
     * Closes the socket if there is no error or the $close_on_error argument is true.
771
     * Implements from rfc 821: QUIT <CRLF>
772
     * @param boolean $close_on_error Should the connection close if an error occurs?
773
     * @access public
774
     * @return boolean
775
     */
776
    public function quit($close_on_error = true)
777
    {
778
        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
779
        $err = $this->error; //Save any error
780
        if ($noerror or $close_on_error) {
781
            $this->close();
782
            $this->error = $err; //Restore any error from the quit command
783
        }
784
        return $noerror;
785
    }
786
787
    /**
788
     * Send an SMTP RCPT command.
789
     * Sets the TO argument to $toaddr.
790
     * Returns true if the recipient was accepted false if it was rejected.
791
     * Implements from rfc 821: RCPT <SP> TO:<forward-path> <CRLF>
792
     * @param string $toaddr The address the message is being sent to
793
     * @access public
794
     * @return boolean
795
     */
796
    public function recipient($toaddr)
797
    {
798
        return $this->sendCommand(
799
            'RCPT TO',
800
            'RCPT TO:<' . $toaddr . '>',
801
            array(250, 251)
802
        );
803
    }
804
805
    /**
806
     * Send an SMTP RSET command.
807
     * Abort any transaction that is currently in progress.
808
     * Implements rfc 821: RSET <CRLF>
809
     * @access public
810
     * @return boolean True on success.
811
     */
812
    public function reset()
813
    {
814
        return $this->sendCommand('RSET', 'RSET', 250);
815
    }
816
817
    /**
818
     * Send a command to an SMTP server and check its return code.
819
     * @param string $command       The command name - not sent to the server
820
     * @param string $commandstring The actual command to send
821
     * @param integer|array $expect     One or more expected integer success codes
822
     * @access protected
823
     * @return boolean True on success.
824
     */
825
    protected function sendCommand($command, $commandstring, $expect)
826
    {
827
        if (!$this->connected()) {
828
            $this->setError("Called $command without being connected");
829
            return false;
830
        }
831
        $this->client_send($commandstring . self::CRLF);
832
833
        $this->last_reply = $this->get_lines();
834
        // Fetch SMTP code and possible error code explanation
835
        $matches = array();
836
        if (preg_match("/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches)) {
837
            $code = $matches[1];
838
            $code_ex = (count($matches) > 2 ? $matches[2] : null);
839
            // Cut off error code from each response line
840
            $detail = preg_replace(
841
                "/{$code}[ -]".($code_ex ? str_replace('.', '\\.', $code_ex).' ' : '')."/m",
842
                '',
843
                $this->last_reply
844
            );
845
        } else {
846
            // Fall back to simple parsing if regex fails
847
            $code = substr($this->last_reply, 0, 3);
848
            $code_ex = null;
849
            $detail = substr($this->last_reply, 4);
850
        }
851
852
        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
853
854
        if (!in_array($code, (array)$expect)) {
855
            $this->setError(
856
                "$command command failed",
857
                $detail,
858
                $code,
859
                $code_ex
860
            );
861
            $this->edebug(
862
                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
863
                self::DEBUG_CLIENT
864
            );
865
            return false;
866
        }
867
868
        $this->setError('');
869
        return true;
870
    }
871
872
    /**
873
     * Send an SMTP SAML command.
874
     * Starts a mail transaction from the email address specified in $from.
875
     * Returns true if successful or false otherwise. If True
876
     * the mail transaction is started and then one or more recipient
877
     * commands may be called followed by a data command. This command
878
     * will send the message to the users terminal if they are logged
879
     * in and send them an email.
880
     * Implements rfc 821: SAML <SP> FROM:<reverse-path> <CRLF>
881
     * @param string $from The address the message is from
882
     * @access public
883
     * @return boolean
884
     */
885
    public function sendAndMail($from)
886
    {
887
        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
888
    }
889
890
    /**
891
     * Send an SMTP VRFY command.
892
     * @param string $name The name to verify
893
     * @access public
894
     * @return boolean
895
     */
896
    public function verify($name)
897
    {
898
        return $this->sendCommand('VRFY', "VRFY $name", array(250, 251));
899
    }
900
901
    /**
902
     * Send an SMTP NOOP command.
903
     * Used to keep keep-alives alive, doesn't actually do anything
904
     * @access public
905
     * @return boolean
906
     */
907
    public function noop()
908
    {
909
        return $this->sendCommand('NOOP', 'NOOP', 250);
910
    }
911
912
    /**
913
     * Send an SMTP TURN command.
914
     * This is an optional command for SMTP that this class does not support.
915
     * This method is here to make the RFC821 Definition complete for this class
916
     * and _may_ be implemented in future
917
     * Implements from rfc 821: TURN <CRLF>
918
     * @access public
919
     * @return boolean
920
     */
921
    public function turn()
922
    {
923
        $this->setError('The SMTP TURN command is not implemented');
924
        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
925
        return false;
926
    }
927
928
    /**
929
     * Send raw data to the server.
930
     * @param string $data The data to send
931
     * @access public
932
     * @return integer|boolean The number of bytes sent to the server or false on error
933
     */
934
    public function client_send($data)
935
    {
936
        $this->edebug("CLIENT -> SERVER: $data", self::DEBUG_CLIENT);
937
        return fwrite($this->smtp_conn, $data);
938
    }
939
940
    /**
941
     * Get the latest error.
942
     * @access public
943
     * @return array
944
     */
945
    public function getError()
946
    {
947
        return $this->error;
948
    }
949
950
    /**
951
     * Get SMTP extensions available on the server
952
     * @access public
953
     * @return array|null
954
     */
955
    public function getServerExtList()
956
    {
957
        return $this->server_caps;
958
    }
959
960
    /**
961
     * A multipurpose method
962
     * The method works in three ways, dependent on argument value and current state
963
     *   1. HELO/EHLO was not sent - returns null and set up $this->error
964
     *   2. HELO was sent
965
     *     $name = 'HELO': returns server name
966
     *     $name = 'EHLO': returns boolean false
967
     *     $name = any string: returns null and set up $this->error
968
     *   3. EHLO was sent
969
     *     $name = 'HELO'|'EHLO': returns server name
970
     *     $name = any string: if extension $name exists, returns boolean True
971
     *       or its options. Otherwise returns boolean False
972
     * In other words, one can use this method to detect 3 conditions:
973
     *  - null returned: handshake was not or we don't know about ext (refer to $this->error)
974
     *  - false returned: the requested feature exactly not exists
975
     *  - positive value returned: the requested feature exists
976
     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
977
     * @return mixed
978
     */
979
    public function getServerExt($name)
980
    {
981
        if (!$this->server_caps) {
982
            $this->setError('No HELO/EHLO was sent');
983
            return null;
984
        }
985
986
        // the tight logic knot ;)
987
        if (!array_key_exists($name, $this->server_caps)) {
988
            if ($name == 'HELO') {
989
                return $this->server_caps['EHLO'];
990
            }
991
            if ($name == 'EHLO' || array_key_exists('EHLO', $this->server_caps)) {
992
                return false;
993
            }
994
            $this->setError('HELO handshake was used. Client knows nothing about server extensions');
995
            return null;
996
        }
997
998
        return $this->server_caps[$name];
999
    }
1000
1001
    /**
1002
     * Get the last reply from the server.
1003
     * @access public
1004
     * @return string
1005
     */
1006
    public function getLastReply()
1007
    {
1008
        return $this->last_reply;
1009
    }
1010
1011
    /**
1012
     * Read the SMTP server's response.
1013
     * Either before eof or socket timeout occurs on the operation.
1014
     * With SMTP we can tell if we have more lines to read if the
1015
     * 4th character is '-' symbol. If it is a space then we don't
1016
     * need to read anything else.
1017
     * @access protected
1018
     * @return string
1019
     */
1020
    protected function get_lines()
1021
    {
1022
        // If the connection is bad, give up straight away
1023
        if (!is_resource($this->smtp_conn)) {
1024
            return '';
1025
        }
1026
        $data = '';
1027
        $endtime = 0;
1028
        stream_set_timeout($this->smtp_conn, $this->Timeout);
1029
        if ($this->Timelimit > 0) {
1030
            $endtime = time() + $this->Timelimit;
1031
        }
1032
        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1033
            $str = @fgets($this->smtp_conn, 515);
1034
            $this->edebug("SMTP -> get_lines(): \$data was \"$data\"", self::DEBUG_LOWLEVEL);
1035
            $this->edebug("SMTP -> get_lines(): \$str is \"$str\"", self::DEBUG_LOWLEVEL);
1036
            $data .= $str;
1037
            $this->edebug("SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL);
1038
            // If 4th character is a space, we are done reading, break the loop, micro-optimisation over strlen
1039
            if ((isset($str[3]) and $str[3] == ' ')) {
1040
                break;
1041
            }
1042
            // Timed-out? Log and break
1043
            $info = stream_get_meta_data($this->smtp_conn);
1044
            if ($info['timed_out']) {
1045
                $this->edebug(
1046
                    'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)',
1047
                    self::DEBUG_LOWLEVEL
1048
                );
1049
                break;
1050
            }
1051
            // Now check if reads took too long
1052
            if ($endtime and time() > $endtime) {
1053
                $this->edebug(
1054
                    'SMTP -> get_lines(): timelimit reached ('.
1055
                    $this->Timelimit . ' sec)',
1056
                    self::DEBUG_LOWLEVEL
1057
                );
1058
                break;
1059
            }
1060
        }
1061
        return $data;
1062
    }
1063
1064
    /**
1065
     * Enable or disable VERP address generation.
1066
     * @param boolean $enabled
1067
     */
1068
    public function setVerp($enabled = false)
1069
    {
1070
        $this->do_verp = $enabled;
1071
    }
1072
1073
    /**
1074
     * Get VERP address generation mode.
1075
     * @return boolean
1076
     */
1077
    public function getVerp()
1078
    {
1079
        return $this->do_verp;
1080
    }
1081
1082
    /**
1083
     * Set error messages and codes.
1084
     * @param string $message The error message
1085
     * @param string $detail Further detail on the error
1086
     * @param string $smtp_code An associated SMTP error code
1087
     * @param string $smtp_code_ex Extended SMTP code
1088
     */
1089
    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1090
    {
1091
        $this->error = array(
1092
            'error' => $message,
1093
            'detail' => $detail,
1094
            'smtp_code' => $smtp_code,
1095
            'smtp_code_ex' => $smtp_code_ex
1096
        );
1097
    }
1098
1099
    /**
1100
     * Set debug output method.
1101
     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it.
1102
     */
1103
    public function setDebugOutput($method = 'echo')
1104
    {
1105
        $this->Debugoutput = $method;
1106
    }
1107
1108
    /**
1109
     * Get debug output method.
1110
     * @return string
1111
     */
1112
    public function getDebugOutput()
1113
    {
1114
        return $this->Debugoutput;
1115
    }
1116
1117
    /**
1118
     * Set debug output level.
1119
     * @param integer $level
1120
     */
1121
    public function setDebugLevel($level = 0)
1122
    {
1123
        $this->do_debug = $level;
1124
    }
1125
1126
    /**
1127
     * Get debug output level.
1128
     * @return integer
1129
     */
1130
    public function getDebugLevel()
1131
    {
1132
        return $this->do_debug;
1133
    }
1134
1135
    /**
1136
     * Set SMTP timeout.
1137
     * @param integer $timeout
1138
     */
1139
    public function setTimeout($timeout = 0)
1140
    {
1141
        $this->Timeout = $timeout;
1142
    }
1143
1144
    /**
1145
     * Get SMTP timeout.
1146
     * @return integer
1147
     */
1148
    public function getTimeout()
1149
    {
1150
        return $this->Timeout;
1151
    }
1152
}