Issues (42)

src/Validate/Email.php (21 issues)

1
<?php
2
3
namespace Validate;
4
5
use Exception;
6
use Validate\Traits\BlockStringTrait;
7
use Validate\Traits\GetDataTrait;
8
9
class Email implements \Validate\Contracts\Validate
10
{
11
    use BlockStringTrait, GetDataTrait;
12
13
    protected $stream = false;
14
15
    /**
16
     * SMTP port number
17
     *
18
     * @var int
19
     */
20
    protected $port = 25;
21
22
    /**
23
     * Email address for request
24
     *
25
     * @var string
26
     */
27
    protected $from = 'root@localhost';
28
29
    /**
30
     * The connection timeout, in seconds.
31
     *
32
     * @var int
33
     */
34
    protected $max_connection_timeout = 30;
35
36
    /**
37
     * Timeout value on stream, in seconds.
38
     *
39
     * @var int
40
     */
41
    protected $stream_timeout = 5;
42
43
    /**
44
     * Wait timeout on stream, in seconds.
45
     * * 0 - not wait
46
     *
47
     * @var int
48
     */
49
    protected $stream_timeout_wait = 0;
50
51
    /**
52
     * Whether to throw exceptions for errors.
53
     *
54
     * @type   boolean
55
     * @access protected
56
     */
57
    protected $exceptions = false;
58
59
    /**
60
     * The number of errors encountered.
61
     *
62
     * @type   integer
63
     * @access protected
64
     */
65
    protected $error_count = 0;
66
67
    /**
68
     * class debug output mode.
69
     *
70
     * @type boolean
71
     */
72
    public $Debug = false;
73
74
    /**
75
     * How to handle debug output.
76
     * Options:
77
     * * `echo` Output plain-text as-is, appropriate for CLI
78
     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
79
     * * `log` Output to error log as configured in php.ini
80
     *
81
     * @type string
82
     */
83
    public $Debugoutput = 'echo';
84
85
    /**
86
     * SMTP RFC standard line ending.
87
     */
88
    const CRLF = "\r\n";
89
90
    /**
91
     * Holds the most recent error message.
92
     *
93
     * @type string
94
     */
95
    public $ErrorInfo = '';
96
97
    /**
98
     * Constructor.
99
     *
100
     * @param boolean $exceptions Should we throw external exceptions?
101
     */
102
    public function __construct($exceptions = false)
103
    {
104
        $this->exceptions = (boolean) $exceptions;
105
    }
106
107
    public static function isSame(string $to, string $from)
108
    {
109
        return (self::toDatabase($to)===self::toDatabase($from));
110
    }
111
112
    /**
113
     * Validate email address.
114
     *
115
     * @param  string $email
116
     * @return boolean True if valid.
117
     */
118
    public static function validate(string $email): boolean
0 ignored issues
show
The type Validate\boolean was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
119
    {
120
        if ((boolean) filter_var($email, FILTER_VALIDATE_EMAIL)===false) {
121
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the type-hinted return Validate\boolean.
Loading history...
122
        }
123
        
124
        $emailAddresse = explode("@", trim($email));
125
126
127
        if (self::foundInMultiplesArrays(
128
            [
129
            [
130
                $emailAddresse[0],
131
                self::getListFromFile('black-names')
132
            ],
133
            [
134
                $emailAddresse[0],
135
                self::getListFromFile('black-first-names')
136
            ],
137
            ]
138
        )
139
        ) {
140
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the type-hinted return Validate\boolean.
Loading history...
141
        }
142
143
        if (!checkdnsrr($emailAddresse[1], "MX")) {
144
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the type-hinted return Validate\boolean.
Loading history...
145
        }
146
147
        return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the type-hinted return Validate\boolean.
Loading history...
148
    }
149
150
    /**
151
     * Email to Database
152
     *
153
     * @param  string $email
154
     * @return string Email
155
     */
156
    public static function toDatabase(string $email): string
157
    {
158
        return $email;
159
    }
160
161
    /**
162
     * Email to User
163
     *
164
     * @param  string $email
165
     * @return string Email
166
     */
167
    public static function toUser(string $email): string
168
    {
169
        return $email;
170
    }
171
172
    /**
173
     * Break Email
174
     *
175
     * @param  string $email
176
     * @return array Email
177
     */
178
    public static function break(string $email): array
179
    {
180
        $email = explode('@', $email);
181
        $data['address'] = $email[0];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.
Loading history...
182
        $data['domain'] = $email[1];
183
        return $data;
184
    }
185
186
    /**
187
     * Set email address for SMTP request
188
     *
189
     * @param  string $email Email address
190
     * @return void
191
     */
192
    public function setEmailFrom(string $email): void
193
    {
194
        if (!self::validate($email)) {
195
            $this->set_error('Invalid address : ' . $email);
196
            $this->edebug($this->ErrorInfo);
197
            if ($this->exceptions) {
198
                throw new Exception($this->ErrorInfo);
199
            }
200
        }
201
        $this->from = $email;
202
    }
203
204
    /**
205
     * Set connection timeout, in seconds.
206
     *
207
     * @param int $seconds
208
     */
209
    public function setConnectionTimeout($seconds): void
210
    {
211
        if ($seconds > 0) {
212
            $this->max_connection_timeout = (int) $seconds;
213
        }
214
    }
215
216
    /**
217
     * Sets the timeout value on stream, expressed in the seconds
218
     *
219
     * @param int $seconds
220
     */
221
    public function setStreamTimeout($seconds): void
222
    {
223
        if ($seconds > 0) {
224
            $this->stream_timeout = (int) $seconds;
225
        }
226
    }
227
228
    public function setStreamTimeoutWait($seconds): void
229
    {
230
        if ($seconds >= 0) {
231
            $this->stream_timeout_wait = (int) $seconds;
232
        }
233
    }
234
235
    /**
236
     * Get array of MX records for host. Sort by weight information.
237
     *
238
     * @param  string $hostname The Internet host name.
239
     * @return array Array of the MX records found.
240
     */
241
    public function getMXrecords(string $hostname): array
242
    {
243
        $mxhosts = array();
244
        $mxweights = array();
245
        if (getmxrr($hostname, $mxhosts, $mxweights) === false) {
246
            $this->set_error('MX records not found or an error occurred');
247
            $this->edebug($this->ErrorInfo);
248
        } else {
249
            array_multisort($mxweights, $mxhosts);
250
        }
251
        /**
252
         * Add A-record as last chance (e.g. if no MX record is there).
253
         * Thanks Nicht Lieb.
254
         *
255
         * @link http://www.faqs.org/rfcs/rfc2821.html RFC 2821 - Simple Mail Transfer Protocol
256
         */
257
        if (empty($mxhosts)) {
258
            $mxhosts[] = $hostname;
259
        }
260
        return $mxhosts;
261
    }
262
263
    /**
264
     * Parses input string to array(0=>user, 1=>domain)
265
     *
266
     * @param  string  $email
267
     * @param  boolean $only_domain
268
     * @return string|array
269
     * @access private
270
     */
271
    private static function parse_email(string $email, boolean $only_domain = true)
272
    {
273
        sscanf($email, "%[^@]@%s", $user, $domain);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $domain does not exist. Did you maybe mean $only_domain?
Loading history...
274
        return ($only_domain) ? $domain : array($user, $domain);
275
    }
276
277
    /**
278
     * Add an error message to the error container.
279
     *
280
     * @access protected
281
     * @param  string $msg
282
     * @return void
283
     */
284
    protected function set_error($msg)
285
    {
286
        $this->error_count++;
287
        $this->ErrorInfo = $msg;
288
    }
289
290
    /**
291
     * Check if an error occurred.
292
     *
293
     * @access public
294
     * @return boolean True if an error did occur.
295
     */
296
    public function isError(): boolean
297
    {
298
        return ($this->error_count > 0);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->error_count > 0 returns the type boolean which is incompatible with the type-hinted return Validate\boolean.
Loading history...
299
    }
300
301
    /**
302
     * Output debugging info
303
     * Only generates output if debug output is enabled
304
     *
305
     * @see   verifyEmail::$Debugoutput
306
     * @see   verifyEmail::$Debug
307
     * @param string $str
308
     */
309
    protected function edebug($str)
310
    {
311
        if (!$this->Debug) {
312
            return;
313
        }
314
        switch ($this->Debugoutput) {
315
        case 'log':
316
            //Don't output, just log
317
            error_log($str);
318
            break;
319
        case 'html':
320
            //Cleans up output a bit for a better looking, HTML-safe output
321
            echo htmlentities(
322
                preg_replace('/[\r\n]+/', '', $str),
323
                ENT_QUOTES,
324
                'UTF-8'
325
            )
326
            . "<br>\n";
327
            break;
328
        case 'echo':
329
        default:
330
            //Normalize line breaks
331
            $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str);
332
            echo gmdate('Y-m-d H:i:s') . "\t" . str_replace(
333
                "\n",
334
                "\n \t ",
335
                trim($str)
336
            ) . "\n";
337
        }
338
    }
339
340
    /**
341
     * Validate email
342
     *
343
     * @param  string $email Email address
344
     * @return boolean True if the valid email also exist
345
     */
346
    public function check(string $email): boolean
347
    {
348
        $result = false;
0 ignored issues
show
The assignment to $result is dead and can be removed.
Loading history...
349
350
        if (!self::validate($email)) {
351
            $this->set_error("{$email} incorrect e-mail");
352
            $this->edebug($this->ErrorInfo);
353
            if ($this->exceptions) {
354
                throw new Exception($this->ErrorInfo);
355
            }
356
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the type-hinted return Validate\boolean.
Loading history...
357
        }
358
        $this->error_count = 0; // Reset errors
359
        $this->stream = false;
360
361
        $mxs = $this->getMXrecords(self::parse_email($email));
0 ignored issues
show
It seems like self::parse_email($email) can also be of type array; however, parameter $hostname of Validate\Email::getMXrecords() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

361
        $mxs = $this->getMXrecords(/** @scrutinizer ignore-type */ self::parse_email($email));
Loading history...
362
        $timeout = ceil($this->max_connection_timeout / count($mxs));
363
        foreach ($mxs as $host) {
364
            /**
365
             * suppress error output from stream socket client...
366
             * Thanks Michael.
367
             */
368
            $this->stream = @stream_socket_client("tcp://" . $host . ":" . $this->port, $errno, $errstr, $timeout);
369
            if ($this->stream === false) {
370
                if ($errno == 0) {
371
                    $this->set_error("Problem initializing the socket");
372
                    $this->edebug($this->ErrorInfo);
373
                    if ($this->exceptions) {
374
                        throw new Exception($this->ErrorInfo);
375
                    }
376
                    return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the type-hinted return Validate\boolean.
Loading history...
377
                } else {
378
                    $this->edebug($host . ":" . $errstr);
379
                }
380
            } else {
381
                stream_set_timeout($this->stream, $this->stream_timeout);
382
                stream_set_blocking($this->stream, 1);
383
384
                if ($this->_streamCode($this->_streamResponse()) == '220') {
385
                    $this->edebug("Connection success {$host}");
386
                    break;
387
                } else {
388
                    fclose($this->stream);
389
                    $this->stream = false;
390
                }
391
            }
392
        }
393
394
        if ($this->stream === false) {
395
            $this->set_error("All connection fails");
396
            $this->edebug($this->ErrorInfo);
397
            if ($this->exceptions) {
398
                throw new Exception($this->ErrorInfo);
399
            }
400
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the type-hinted return Validate\boolean.
Loading history...
401
        }
402
403
        $this->_streamQuery("HELO " . self::parse_email($this->from));
0 ignored issues
show
Are you sure self::parse_email($this->from) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

403
        $this->_streamQuery("HELO " . /** @scrutinizer ignore-type */ self::parse_email($this->from));
Loading history...
404
        $this->_streamResponse();
405
        $this->_streamQuery("MAIL FROM: <{$this->from}>");
406
        $this->_streamResponse();
407
        $this->_streamQuery("RCPT TO: <{$email}>");
408
        $code = $this->_streamCode($this->_streamResponse());
409
        $this->_streamResponse();
410
        $this->_streamQuery("RSET");
411
        $this->_streamResponse();
412
        $code2 = $this->_streamCode($this->_streamResponse());
413
        $this->_streamQuery("QUIT");
414
        fclose($this->stream);
415
        
416
        $code = !empty($code2)?$code2:$code;
417
        switch ($code) {
418
        case '250':
419
            /**
420
             * http://www.ietf.org/rfc/rfc0821.txt
421
             * 250 Requested mail action okay, completed
422
             * email address was accepted
423
             */
424
            // no break
425
        case '450':
426
        case '451':
427
        case '452':
428
            /**
429
             * http://www.ietf.org/rfc/rfc0821.txt
430
             * 450 Requested action not taken: the remote mail server
431
             * does not want to accept mail from your server for
432
             * some reason (IP address, blacklisting, etc..)
433
             * Thanks Nicht Lieb.
434
             * 451 Requested action aborted: local error in processing
435
             * 452 Requested action not taken: insufficient system storage
436
             * email address was greylisted (or some temporary error occured on the MTA)
437
             * i believe that e-mail exists
438
             */
439
            return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the type-hinted return Validate\boolean.
Loading history...
440
        case '550':
441
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the type-hinted return Validate\boolean.
Loading history...
442
        default:
443
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the type-hinted return Validate\boolean.
Loading history...
444
        }
445
    }
446
447
    /**
448
     * writes the contents of string to the file stream pointed to by handle
449
     * If an error occurs, returns FALSE.
450
     *
451
     * @access protected
452
     * @param  string $string The string that is to be written
453
     * @return string Returns a result code, as an integer.
454
     */
455
    protected function _streamQuery(string $query)
456
    {
457
        $this->edebug($query);
458
        return stream_socket_sendto($this->stream, $query . self::CRLF);
0 ignored issues
show
$this->stream of type boolean is incompatible with the type resource expected by parameter $socket of stream_socket_sendto(). ( Ignorable by Annotation )

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

458
        return stream_socket_sendto(/** @scrutinizer ignore-type */ $this->stream, $query . self::CRLF);
Loading history...
459
    }
460
461
    /**
462
     * Reads all the line long the answer and analyze it.
463
     * If an error occurs, returns FALSE
464
     *
465
     * @access protected
466
     * @return string Response
467
     */
468
    protected function _streamResponse($timed = 0)
469
    {
470
        $reply = stream_get_line($this->stream, 1);
0 ignored issues
show
$this->stream of type boolean is incompatible with the type resource expected by parameter $stream of stream_get_line(). ( Ignorable by Annotation )

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

470
        $reply = stream_get_line(/** @scrutinizer ignore-type */ $this->stream, 1);
Loading history...
471
        $status = stream_get_meta_data($this->stream);
0 ignored issues
show
$this->stream of type boolean is incompatible with the type resource expected by parameter $stream of stream_get_meta_data(). ( Ignorable by Annotation )

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

471
        $status = stream_get_meta_data(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
472
473
        if (!empty($status['timed_out'])) {
474
            $this->edebug("Timed out while waiting for data! (timeout {$this->stream_timeout} seconds)");
475
        }
476
477
        if ($reply === false && $status['timed_out'] && $timed < $this->stream_timeout_wait) {
478
            return $this->_streamResponse($timed + $this->stream_timeout);
479
        }
480
481
482
        if ($reply !== false && $status['unread_bytes'] > 0) {
483
            $reply .= stream_get_line($this->stream, $status['unread_bytes'], self::CRLF);
484
        }
485
        $this->edebug($reply);
486
        return $reply;
487
    }
488
489
    /**
490
     * Get Response code from Response
491
     *
492
     * @param  string $str
493
     * @return string
494
     */
495
    protected function _streamCode(string $str): string
496
    {
497
        preg_match('/^(?<code>[0-9]{3})(\s|-)(.*)$/ims', $str, $matches);
498
        $code = isset($matches['code']) ? $matches['code'] : false;
499
        return $code;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $code could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
500
    }
501
}
502