Completed
Push — master ( 7646f6...24db39 )
by Lars
15:46
created

EmailCheck::punnycode()   B

Complexity

Conditions 8
Paths 32

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 8.2964

Importance

Changes 0
Metric Value
dl 0
loc 42
ccs 15
cts 18
cp 0.8333
rs 8.0035
c 0
b 0
f 0
cc 8
nc 32
nop 2
crap 8.2964
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
    public static function isDnsError(string $domain): bool
47
    {
48
        if (\function_exists('checkdnsrr')) {
49
            /** @noinspection IfReturnReturnSimplificationInspection */
50
            $mxFound = \checkdnsrr($domain . '.', 'MX');
51
            if ($mxFound === true) {
52 151
                return false;
53
            }
54 151
55 2
            $aFound = \checkdnsrr($domain . '.', 'A');
56
            /** @noinspection IfReturnReturnSimplificationInspection */
57
            if ($aFound === true) {
58
                return false;
59 150
            }
60
61 150
            return true;
62
        }
63 150
64
        throw new \Exception(' Can\'t call checkdnsrr');
65 3
    }
66
67 149
    /**
68
     * Check if the domain is a example domain.
69 149
     *
70
     * @param string $domain
71 149
     *
72
     * @return bool
73
     */
74 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 149
    {
76
        if (self::$domainsExample === null) {
77
            self::$domainsExample = self::getData('domainsExample');
78 149
        }
79
80
        if (\in_array($domain, self::$domainsExample, true)) {
81
            return true;
82 149
        }
83
84 149
        return false;
85
    }
86 24
87
    /**
88
     * Check if the domain is a temporary domain.
89 128
     *
90 9
     * @param string $domain
91
     *
92
     * @return bool
93 119
     */
94 119
    public static function isTemporaryDomain(string $domain): bool
95
    {
96 119
        if (self::$domainsTemporary === null) {
97 2
            self::$domainsTemporary = self::getData('domainsTemporary');
98
        }
99
100 118
        if (\in_array($domain, self::$domainsTemporary, true)) {
101
            return true;
102
        }
103
104
        if (\preg_match('/.*\.(?:tk|ml|ga|cf|gq)$/si', $domain)) {
105 118
            return true;
106
        }
107
108 118
        return false;
109 118
    }
110 17
111 17
    /**
112 17
     * Check if the domain has a typo.
113 17
     *
114 17
     * @param string $domain
115 17
     *
116
     * @return bool
117 17
     */
118 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
        if (self::$domainsTypo === null) {
121
            self::$domainsTypo = self::getData('domainsTypo');
122 118
        }
123
124 118
        if (\in_array($domain, self::$domainsTypo, true)) {
125
            return true;
126 13
        }
127
128
        return false;
129 106
    }
130
131 106
    /**
132 17
     * Check if the email is valid.
133
     *
134
     * @param string $email
135 106
     * @param bool   $useExampleDomainCheck
136
     * @param bool   $useTypoInDomainCheck
137 106
     * @param bool   $useTemporaryDomainCheck
138 63
     * @param bool   $useDnsCheck             (do not use, if you don't need it)
139
     *
140
     * @return bool
141 44
     */
142 2
    public static function isValid(string $email, bool $useExampleDomainCheck = false, bool $useTypoInDomainCheck = false, bool $useTemporaryDomainCheck = false, bool $useDnsCheck = false): bool
143
    {
144
        if (!isset($email[0])) {
145 44
            return false;
146 1
        }
147
148
        // make sure string length is limited to avoid DOS attacks
149 44
        $emailStringLength = \strlen($email);
150
        if (
151
            $emailStringLength >= 320
152
            ||
153 44
            $emailStringLength <= 2 // i@y //
154 3
        ) {
155
            return false;
156
        }
157 41
        unset($emailStringLength);
158
159
        $email = \str_replace(
160
            [
161
                '.', // 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
                '.',
166
                '@',
167 2
            ],
168
            $email
169 2
        );
170 1
171
        if (
172
            (\strpos($email, '@') === false) // "at" is needed
173 2
            ||
174 2
            (\strpos($email, '.') === false && \strpos($email, ':') === false) // "dot" or "colon" is needed
175
        ) {
176
            return false;
177 2
        }
178
179
        if (!\preg_match('/^(?<local>.*<?)(?:.*)@(?<domain>.*)(?:>?)$/', $email, $parts)) {
180
            return false;
181
        }
182
183
        $local = $parts['local'];
184
        $domain = $parts['domain'];
185
186
        if (!$local) {
187 3
            return false;
188
        }
189 3
190 1
        if (!$domain) {
191
            return false;
192
        }
193 3
194 2
        // Escaped spaces are allowed in the "local"-part.
195
        $local = \str_replace('\\ ', '', $local);
196
197 3
        // Spaces in quotes e.g. "firstname lastname"@foo.bar are also allowed in the "local"-part.
198
        $quoteHelperForIdn = false;
199
        if (\preg_match('/^"(?<inner>[^"]*)"$/mU', $local, $parts)) {
200
            $quoteHelperForIdn = true;
201
            $local = \trim(
202
                \str_replace(
203
                    $parts['inner'],
204
                    \str_replace(' ', '', $parts['inner']),
205
                    $local
206
                ),
207 2
                '"'
208
            );
209 2
        }
210 1
211
        if (
212
            \strpos($local, ' ') !== false // no spaces allowed, anymore
213 2
            ||
214 1
            \strpos($local, '".') !== false // no quote + dot allowed
215
        ) {
216
            return false;
217 2
        }
218 1
219
        list($local, $domain) = self::punnycode($local, $domain);
220
221 2
        if ($quoteHelperForIdn === true) {
222
            $local = '"' . $local . '"';
223
        }
224
225
        $email = $local . '@' . $domain;
226
227
        if (!\filter_var($email, \FILTER_VALIDATE_EMAIL)) {
228
            return false;
229
        }
230
231 1
        if ($useExampleDomainCheck === true && self::isExampleDomain($domain) === true) {
232
            return false;
233 1
        }
234 1
235
        if ($useTypoInDomainCheck === true && self::isTypoInDomain($domain) === true) {
236 1
            return false;
237
        }
238
239
        if ($useTemporaryDomainCheck === true && self::isTemporaryDomain($domain) === true) {
240
            return false;
241
        }
242
243
        if ($useDnsCheck === true && self::isDnsError($domain) === true) {
244
            return false;
245
        }
246
247
        return true;
248
    }
249
250
    /**
251 4
     * get data from "/data/*.php"
252
     *
253 4
     * @param string $file
254
     *
255 4
     * @return array|bool|int|string <p>Will return false on error.</p>
256 4
     */
257 1
    protected static function getData(string $file)
258
    {
259
        $file = __DIR__ . '/data/' . $file . '.php';
260 4
        if (\file_exists($file)) {
261
            /** @noinspection PhpIncludeInspection */
262 4
            return require $file;
263
        }
264
265
        return false;
266 4
    }
267
268
    /**
269
     * @param string $local
270
     * @param string $domain
271
     *
272
     * @return array
273
     */
274
    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 106
        if (
279
            \defined('IDNA_NONTRANSITIONAL_TO_ASCII')
280 106
            &&
281
            \defined('INTL_IDNA_VARIANT_UTS46')
282
            &&
283
            \constant('IDNA_NONTRANSITIONAL_TO_ASCII')
284 106
        ) {
285
            $useIdnaUts46 = true;
286 106
        } else {
287
            $useIdnaUts46 = false;
288 106
        }
289
290 106
        if ($useIdnaUts46 === true) {
291
            /** @noinspection PhpComposerExtensionStubsInspection */
292
            $localTmp = \idn_to_ascii($local, \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46);
293
        } else {
294
            /** @noinspection PhpComposerExtensionStubsInspection */
295 106
            $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
            $local = $localTmp;
299
        }
300
        unset($localTmp);
301
302 106
        if ($useIdnaUts46 === true) {
303 99
            /** @noinspection PhpComposerExtensionStubsInspection */
304
            $domainTmp = \idn_to_ascii($domain, \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46);
305 106
        } else {
306
            /** @noinspection PhpComposerExtensionStubsInspection */
307 106
            $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
            $domain = $domainTmp;
311
        }
312
        unset($domainTmp);
313
314 106
        return [$local, $domain];
315 102
    }
316
}
317