Issues (4069)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

include/phpmailer/class.smtp.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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