Completed
Push — master ( b4e9b7...cff413 )
by Peter
11:12
created

EmailValidator   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 126
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 43.24%

Importance

Changes 0
Metric Value
wmc 15
lcom 1
cbo 7
dl 0
loc 126
ccs 16
cts 37
cp 0.4324
rs 10
c 0
b 0
f 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
B isValid() 0 40 10
A checkMxPorts() 0 25 5
1
<?php
2
3
/**
4
 * This software package is licensed under AGPL or Commercial license.
5
 *
6
 * @package maslosoft/mangan
7
 * @licence AGPL or Commercial
8
 * @copyright Copyright (c) Piotr Masełkowski <[email protected]>
9
 * @copyright Copyright (c) Maslosoft
10
 * @copyright Copyright (c) Others as mentioned in code
11
 * @link https://maslosoft.com/mangan/
12
 */
13
14
namespace Maslosoft\Mangan\Validators\BuiltIn;
15
16
use Maslosoft\Addendum\Interfaces\AnnotatedInterface;
17
use Maslosoft\Mangan\Interfaces\Validators\ValidatorInterface;
18
use Maslosoft\Mangan\Meta\ManganMeta;
19
use Maslosoft\Mangan\Validators\Traits\AllowEmpty;
20
use Maslosoft\Mangan\Validators\Traits\Messages;
21
use Maslosoft\Mangan\Validators\Traits\OnScenario;
22
use Maslosoft\Mangan\Validators\Traits\Safe;
23
use Maslosoft\Mangan\Validators\Traits\SkipOnError;
24
25
/**
26
 * EmailValidator
27
 *
28
 * @author Piotr Maselkowski <pmaselkowski at gmail.com>
29
 */
30
class EmailValidator implements ValidatorInterface
31
{
32
33
	use AllowEmpty,
34
	  Messages,
35
	  OnScenario,
36
	  Safe,
37
	  SkipOnError;
38
39
	const EmailPattern = '/^[\p{L}0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\.[\p{L}0-9!#$%&\'*+\\/=?^_`{|}~-]+)*@(?:[\p{L}0-9](?:[\p{L}0-9-]*[\p{L}0-9])?\.)+[\p{L}0-9](?:[\p{L}0-9-]*[\p{L}0-9])?$/u';
40
	const FullEmailPattern = '/^[^@]*<[\p{L}0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\\.[\p{L}0-9!#$%&\'*+\\/=?^_`{|}~-]+)*@(?:[\p{L}0-9](?:[\p{L}0-9-]*[\p{L}0-9])?\\.)+[\p{L}0-9](?:[\p{L}0-9-]*[\p{L}0-9])?>$/u';
41
42
	/**
43
	 * @var string the regular expression used to validate the attribute value.
44
	 * @see http://www.regular-expressions.info/email.html
45
	 */
46
	public $pattern = self::EmailPattern;
47
48
	/**
49
	 * @var boolean whether to check the MX record for the email address.
50
	 * Defaults to false. To enable it, you need to make sure the PHP function 'checkdnsrr'
51
	 * exists in your PHP installation.
52
	 * Please note that this check may fail due to temporary problems even if email is deliverable.
53
	 */
54
	public $checkMX = false;
55
56
	/**
57
	 * @var boolean whether to check port 25 for the email address.
58
	 * Defaults to false. To enable it, ensure that the PHP functions 'dns_get_record' and
59
	 * 'fsockopen' are available in your PHP installation.
60
	 * Please note that this check may fail due to temporary problems even if email is deliverable.
61
	 */
62
	public $checkPort = false;
63
64
	/**
65
	 * @Label('{attribute} must be valid email address')
66
	 * @var string
67
	 */
68
	public $msgValid = '';
69
70
	/**
71
	 * @Label('Email domain "{domain}" does not exists')
72
	 * @var string
73
	 */
74
	public $msgDomain = '';
75
76
	/**
77
	 * @Label('Email service does not seem to be running at "{domain}"')
78
	 * @var string
79
	 */
80
	public $msgPort = '';
81
82 6
	public function isValid(AnnotatedInterface $model, $attribute)
83
	{
84 6
		if ($this->allowEmpty && empty($model->$attribute))
85
		{
86 2
			return true;
87
		}
88 4
		$label = ManganMeta::create($model)->field($attribute)->label;
89
90 4
		if (!is_scalar($model->$attribute))
91
		{
92
			$this->addError('msgValid', ['{attribute}' => $label]);
93
			return false;
94
		}
95 4
		if (!preg_match($this->pattern, $model->$attribute))
96
		{
97 1
			$this->addError('msgValid', ['{attribute}' => $label]);
98 1
			return false;
99
		}
100 4
		$domain = rtrim(substr($model->$attribute, strpos($model->$attribute, '@') + 1), '>');
101 4
		if ($this->checkMX)
102
		{
103 1
			if (function_exists('checkdnsrr'))
104
			{
105 1
				if (!checkdnsrr($domain, 'MX'))
106
				{
107 1
					$this->addError('msgDomain', ['{domain}' => $domain]);
108 1
					return false;
109
				}
110
			}
111
		}
112 4
		if ($this->checkPort)
113
		{
114
			if ($this->checkMxPorts($domain))
115
			{
116
				$this->addError('msgPort', ['{domain}' => $domain]);
117
				return false;
118
			}
119
		}
120 4
		return true;
121
	}
122
123
	/**
124
	 * Retrieves the list of MX records for $domain and checks if port 25
125
	 * is opened on any of these.
126
	 * @param string $domain domain to be checked
127
	 * @return boolean true if a reachable MX server has been found
128
	 */
129
	protected function checkMxPorts($domain)
130
	{
131
		$records = dns_get_record($domain, DNS_MX);
132
		if ($records === false || empty($records))
133
		{
134
			return false;
135
		}
136
		$sort = function ($a, $b)
137
		{
138
			return $a['pri'] - $b['pri'];
139
		};
140
		usort($records, $sort);
141
		foreach ($records as $record)
142
		{
143
			$errno = null;
144
			$errstr = null;
145
			$handle = @fsockopen($record['target'], 25, $errno, $errstr, 3);
146
			if ($handle !== false)
147
			{
148
				fclose($handle);
149
				return true;
150
			}
151
		}
152
		return false;
153
	}
154
155
}
156