Completed
Push — master ( 8e58d2...d3bb44 )
by Lars
03:02
created

EmailCheck::getData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

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