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

EmailCheck::isValid()   F

Complexity

Conditions 23
Paths 32

Size

Total Lines 108

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 23.0318

Importance

Changes 0
Metric Value
dl 0
loc 108
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
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