Completed
Pull Request — master (#10)
by
unknown
05:06
created

EmailCheck   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 324
Duplicated Lines 7.41 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 85.22%

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 1
dl 24
loc 324
ccs 98
cts 115
cp 0.8522
rs 7.92
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
F isValid() 0 107 23
A isExampleDomain() 12 12 3
A isTypoInDomain() 12 12 3
A isTemporaryDomain() 0 16 4
A getData() 0 10 2
A isDnsError() 0 20 4
C punnycode() 0 64 12

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like EmailCheck often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EmailCheck, and based on these observations, apply Extract Interface, too.

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