Completed
Push — master ( f9228c...68a9ac )
by Lars
07:25
created

EmailCheck   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 305
Duplicated Lines 11.8 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 84.76%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 1
dl 36
loc 305
ccs 89
cts 105
cp 0.8476
rs 8.48
c 0
b 0
f 0

7 Methods

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