Completed
Push — master ( 402a24...d79d44 )
by Lars
14:30
created

EmailCheck::isValid()   F

Complexity

Conditions 23
Paths 32

Size

Total Lines 107

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 23.0318

Importance

Changes 0
Metric Value
dl 0
loc 107
ccs 49
cts 51
cp 0.9608
rs 3.3333
c 0
b 0
f 0
cc 23
nc 32
nop 5
crap 23.0318

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
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * E-Mail Check Class
9
 *
10
 * -> use "EmailCheck::isValid()" to validate a email-address
11
 *
12
 * @copyright   Copyright (c) 2018, Lars Moelleken (http://moelleken.org/)
13
 * @license     http://opensource.org/licenses/MIT	MIT License
14
 */
15
class EmailCheck
16
{
17
    /**
18
     * @var array|null
19
     */
20
    protected static $domainsExample = null;
21
22
    /**
23
     * @var array|null
24
     */
25
    protected static $domainsTemporary = null;
26
27
    /**
28
     * @var array|null
29
     */
30
    protected static $domainsTypo = null;
31
32
    /**
33
     * @var bool
34
     */
35
    protected static $useDnsCheck = true;
36
37
    /**
38
     * Check if the domain has a MX- or A-record in the DNS.
39
     *
40
     * @param string $domain
41
     *
42
     * @throws \Exception
43
     *
44
     * @return bool
45
     */
46 4
    public static function isDnsError(string $domain): bool
47
    {
48 4
        if (\function_exists('checkdnsrr')) {
49
            /** @noinspection IfReturnReturnSimplificationInspection */
50 4
            $mxFound = \checkdnsrr($domain . '.', 'MX');
51 4
            if ($mxFound === true) {
52 1
                return false;
53
            }
54
55 4
            $aFound = \checkdnsrr($domain . '.', 'A');
56
            /** @noinspection IfReturnReturnSimplificationInspection */
57 4
            if ($aFound === true) {
58
                return false;
59
            }
60
61 4
            return true;
62
        }
63
64
        throw new \Exception(' Can\'t call checkdnsrr');
65
    }
66
67
    /**
68
     * Check if the domain is a example domain.
69
     *
70
     * @param string $domain
71
     *
72
     * @return bool
73
     */
74 2 View Code Duplication
    public static function isExampleDomain(string $domain): bool
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...
75
    {
76 2
        if (self::$domainsExample === null) {
77 1
            self::$domainsExample = self::getData('domainsExample');
78
        }
79
80 2
        if (\in_array($domain, self::$domainsExample, true)) {
81 2
            return true;
82
        }
83
84 2
        return false;
85
    }
86
87
    /**
88
     * Check if the domain is a temporary domain.
89
     *
90
     * @param string $domain
91
     *
92
     * @return bool
93
     */
94 2
    public static function isTemporaryDomain(string $domain): bool
95
    {
96 2
        if (self::$domainsTemporary === null) {
97 1
            self::$domainsTemporary = self::getData('domainsTemporary');
98
        }
99
100 2
        if (\in_array($domain, self::$domainsTemporary, true)) {
101 1
            return true;
102
        }
103
104 2
        if (\preg_match('/.*\.(?:tk|ml|ga|cf|gq)$/si', $domain)) {
105 1
            return true;
106
        }
107
108 2
        return false;
109
    }
110
111
    /**
112
     * Check if the domain has a typo.
113
     *
114
     * @param string $domain
115
     *
116
     * @return bool
117
     */
118 3 View Code Duplication
    public static function isTypoInDomain(string $domain): bool
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...
119
    {
120 3
        if (self::$domainsTypo === null) {
121 1
            self::$domainsTypo = self::getData('domainsTypo');
122
        }
123
124 3
        if (\in_array($domain, self::$domainsTypo, true)) {
125 2
            return true;
126
        }
127
128 3
        return false;
129
    }
130
131
    /**
132
     * Check if the email is valid.
133
     *
134
     * @param string $email
135
     * @param bool   $useExampleDomainCheck
136
     * @param bool   $useTypoInDomainCheck
137
     * @param bool   $useTemporaryDomainCheck
138
     * @param bool   $useDnsCheck             (do not use, if you don't need it)
139
     *
140
     * @return bool
141
     */
142 151
    public static function isValid(string $email, bool $useExampleDomainCheck = false, bool $useTypoInDomainCheck = false, bool $useTemporaryDomainCheck = false, bool $useDnsCheck = false): bool
143
    {
144 151
        if (!isset($email[0])) {
145 2
            return false;
146
        }
147
148
        // make sure string length is limited to avoid DOS attacks
149 150
        $emailStringLength = \strlen($email);
150
        if (
151 150
            $emailStringLength >= 320
152
            ||
153 150
            $emailStringLength <= 2 // i@y //
154
        ) {
155 2
            return false;
156
        }
157 149
        unset($emailStringLength);
158
159 149
        $email = \str_replace(
160
            [
161 149
                '.', // non-Latin chars are also allowed | https://tools.ietf.org/html/rfc6530
162
                '@', // non-Latin chars are also allowed | https://tools.ietf.org/html/rfc6530
163
            ],
164
            [
165 149
                '.',
166
                '@',
167
            ],
168 149
            $email
169
        );
170
171
        if (
172 149
            (\strpos($email, '@') === false) // "at" is needed
173
            ||
174 149
            (\strpos($email, '.') === false && \strpos($email, ':') === false) // "dot" or "colon" is needed
175
        ) {
176 24
            return false;
177
        }
178
179 128
        if (!\preg_match('/^(?<local>.*<?)(?:.*)@(?<domain>.*)(?:>?)$/', $email, $parts)) {
180 9
            return false;
181
        }
182
183 119
        $local = $parts['local'];
184 119
        $domain = $parts['domain'];
185
186 119
        if (!$local) {
187 2
            return false;
188
        }
189
190 118
        if (!$domain) {
191
            return false;
192
        }
193
194
        // Escaped spaces are allowed in the "local"-part.
195 118
        $local = \str_replace('\\ ', '', $local);
196
197
        // Spaces in quotes e.g. "firstname lastname"@foo.bar are also allowed in the "local"-part.
198 118
        $quoteHelperForIdn = false;
199 118
        if (\preg_match('/^"(?<inner>[^"]*)"$/mU', $local, $parts)) {
200 17
            $quoteHelperForIdn = true;
201 17
            $local = \trim(
202 17
                \str_replace(
203 17
                    $parts['inner'],
204 17
                    \str_replace(' ', '', $parts['inner']),
205 17
                    $local
206
                ),
207 17
                '"'
208
            );
209
        }
210
211
        if (
212 118
            \strpos($local, ' ') !== false // no spaces allowed, anymore
213
            ||
214 118
            \strpos($local, '".') !== false // no quote + dot allowed
215
        ) {
216 13
            return false;
217
        }
218
219 106
        list($local, $domain) = self::punnycode($local, $domain);
220
221 106
        if ($quoteHelperForIdn === true) {
222 17
            $local = '"' . $local . '"';
223
        }
224
225 106
        $email = $local . '@' . $domain;
226
227 106
        if (!\filter_var($email, \FILTER_VALIDATE_EMAIL)) {
228 63
            return false;
229
        }
230
231 44
        if ($useExampleDomainCheck === true && self::isExampleDomain($domain) === true) {
232 2
            return false;
233
        }
234
235 44
        if ($useTypoInDomainCheck === true && self::isTypoInDomain($domain) === true) {
236 1
            return false;
237
        }
238
239 44
        if ($useTemporaryDomainCheck === true && self::isTemporaryDomain($domain) === true) {
240
            return false;
241
        }
242
243 44
        if ($useDnsCheck === true && self::isDnsError($domain) === true) {
244 3
            return false;
245
        }
246
247 41
        return true;
248
    }
249
250
    /**
251
     * get data from "/data/*.php"
252
     *
253
     * @param string $file
254
     *
255
     * @return array|bool|int|string <p>Will return false on error.</p>
256
     */
257 1
    protected static function getData(string $file)
258
    {
259 1
        $file = __DIR__ . '/data/' . $file . '.php';
260 1
        if (\file_exists($file)) {
261
            /** @noinspection PhpIncludeInspection */
262 1
            return require $file;
263
        }
264
265
        return false;
266
    }
267
268
    /**
269
     * @param string $local
270
     * @param string $domain
271
     *
272
     * @return array
273
     */
274 106
    private static function punnycode(string $local, string $domain): array
275
    {
276
277
        // https://git.ispconfig.org/ispconfig/ispconfig3/blob/master/interface/lib/classes/functions.inc.php#L305
278
        if (
279
            \defined('IDNA_NONTRANSITIONAL_TO_ASCII')
280
            &&
281
            \defined('INTL_IDNA_VARIANT_UTS46')
282
            &&
283 106
            \constant('IDNA_NONTRANSITIONAL_TO_ASCII')
284
        ) {
285 106
            $useIdnaUts46 = true;
286
        } else {
287
            $useIdnaUts46 = false;
288
        }
289
290 106
        if ($useIdnaUts46 === true) {
291
            /** @noinspection PhpComposerExtensionStubsInspection */
292 106
            $localTmp = \idn_to_ascii($local, \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46);
293
        } else {
294
            /** @noinspection PhpComposerExtensionStubsInspection */
295
            $localTmp = \idn_to_ascii($local);
296
        }
297 106
        if ($localTmp) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $localTmp of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
298 99
            $local = $localTmp;
299
        }
300 106
        unset($localTmp);
301
302 106
        if ($useIdnaUts46 === true) {
303
            /** @noinspection PhpComposerExtensionStubsInspection */
304 106
            $domainTmp = \idn_to_ascii($domain, \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46);
305
        } else {
306
            /** @noinspection PhpComposerExtensionStubsInspection */
307
            $domainTmp = \idn_to_ascii($domain);
308
        }
309 106
        if ($domainTmp) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $domainTmp of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
310 102
            $domain = $domainTmp;
311
        }
312 106
        unset($domainTmp);
313
314 106
        return [$local, $domain];
315
    }
316
}
317