EmailValidator.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\validators;
  8. use Yii;
  9. use yii\base\InvalidConfigException;
  10. use yii\helpers\Json;
  11. use yii\web\JsExpression;
  12. /**
  13. * EmailValidator validates that the attribute value is a valid email address.
  14. *
  15. * @author Qiang Xue <qiang.xue@gmail.com>
  16. * @since 2.0
  17. */
  18. class EmailValidator extends Validator
  19. {
  20. /**
  21. * @var string the regular expression used to validate the attribute value.
  22. * @see http://www.regular-expressions.info/email.html
  23. */
  24. public $pattern = '/^[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/';
  25. /**
  26. * @var string the regular expression used to validate email addresses with the name part.
  27. * This property is used only when [[allowName]] is true.
  28. * @see allowName
  29. */
  30. public $fullPattern = '/^[^@]*<[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?>$/';
  31. /**
  32. * @var bool whether to allow name in the email address (e.g. "John Smith <john.smith@example.com>"). Defaults to false.
  33. * @see fullPattern
  34. */
  35. public $allowName = false;
  36. /**
  37. * @var bool whether to check whether the email's domain exists and has either an A or MX record.
  38. * Be aware that this check can fail due to temporary DNS problems even if the email address is
  39. * valid and an email would be deliverable. Defaults to false.
  40. */
  41. public $checkDNS = false;
  42. /**
  43. * @var bool whether validation process should take into account IDN (internationalized domain
  44. * names). Defaults to false meaning that validation of emails containing IDN will always fail.
  45. * Note that in order to use IDN validation you have to install and enable `intl` PHP extension,
  46. * otherwise an exception would be thrown.
  47. */
  48. public $enableIDN = false;
  49. /**
  50. * {@inheritdoc}
  51. */
  52. public function init()
  53. {
  54. parent::init();
  55. if ($this->enableIDN && !function_exists('idn_to_ascii')) {
  56. throw new InvalidConfigException('In order to use IDN validation intl extension must be installed and enabled.');
  57. }
  58. if ($this->message === null) {
  59. $this->message = Yii::t('yii', '{attribute} is not a valid email address.');
  60. }
  61. }
  62. /**
  63. * {@inheritdoc}
  64. */
  65. protected function validateValue($value)
  66. {
  67. if (!is_string($value)) {
  68. $valid = false;
  69. } elseif (!preg_match('/^(?P<name>(?:"?([^"]*)"?\s)?)(?:\s+)?(?:(?P<open><?)((?P<local>.+)@(?P<domain>[^>]+))(?P<close>>?))$/i', $value, $matches)) {
  70. $valid = false;
  71. } else {
  72. if ($this->enableIDN) {
  73. $matches['local'] = $this->idnToAscii($matches['local']);
  74. $matches['domain'] = $this->idnToAscii($matches['domain']);
  75. $value = $matches['name'] . $matches['open'] . $matches['local'] . '@' . $matches['domain'] . $matches['close'];
  76. }
  77. if (strlen($matches['local']) > 64) {
  78. // The maximum total length of a user name or other local-part is 64 octets. RFC 5322 section 4.5.3.1.1
  79. // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1
  80. $valid = false;
  81. } elseif (strlen($matches['local'] . '@' . $matches['domain']) > 254) {
  82. // There is a restriction in RFC 2821 on the length of an address in MAIL and RCPT commands
  83. // of 254 characters. Since addresses that do not fit in those fields are not normally useful, the
  84. // upper limit on address lengths should normally be considered to be 254.
  85. //
  86. // Dominic Sayers, RFC 3696 erratum 1690
  87. // http://www.rfc-editor.org/errata_search.php?eid=1690
  88. $valid = false;
  89. } else {
  90. $valid = preg_match($this->pattern, $value) || ($this->allowName && preg_match($this->fullPattern, $value));
  91. if ($valid && $this->checkDNS) {
  92. $valid = $this->isDNSValid($matches['domain']);
  93. }
  94. }
  95. }
  96. return $valid ? null : [$this->message, []];
  97. }
  98. /**
  99. * @param string $domain
  100. * @return bool if DNS records for domain are valid
  101. * @see https://github.com/yiisoft/yii2/issues/17083
  102. */
  103. protected function isDNSValid($domain)
  104. {
  105. if (checkdnsrr($domain . '.', 'MX')) {
  106. $mxRecords = dns_get_record($domain . '.', DNS_MX);
  107. if ($mxRecords !== false && count($mxRecords) > 0) {
  108. return true;
  109. }
  110. }
  111. if (checkdnsrr($domain . '.', 'A')) {
  112. $aRecords = dns_get_record($domain . '.', DNS_A);
  113. if ($aRecords !== false && count($aRecords) > 0) {
  114. return true;
  115. }
  116. }
  117. return false;
  118. }
  119. private function idnToAscii($idn)
  120. {
  121. if (PHP_VERSION_ID < 50600) {
  122. // TODO: drop old PHP versions support
  123. return idn_to_ascii($idn);
  124. }
  125. return idn_to_ascii($idn, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
  126. }
  127. /**
  128. * {@inheritdoc}
  129. */
  130. public function clientValidateAttribute($model, $attribute, $view)
  131. {
  132. ValidationAsset::register($view);
  133. if ($this->enableIDN) {
  134. PunycodeAsset::register($view);
  135. }
  136. $options = $this->getClientOptions($model, $attribute);
  137. return 'yii.validation.email(value, messages, ' . Json::htmlEncode($options) . ');';
  138. }
  139. /**
  140. * {@inheritdoc}
  141. */
  142. public function getClientOptions($model, $attribute)
  143. {
  144. $options = [
  145. 'pattern' => new JsExpression($this->pattern),
  146. 'fullPattern' => new JsExpression($this->fullPattern),
  147. 'allowName' => $this->allowName,
  148. 'message' => $this->formatMessage($this->message, [
  149. 'attribute' => $model->getAttributeLabel($attribute),
  150. ]),
  151. 'enableIDN' => (bool) $this->enableIDN,
  152. ];
  153. if ($this->skipOnEmpty) {
  154. $options['skipOnEmpty'] = 1;
  155. }
  156. return $options;
  157. }
  158. }