| Total Complexity | 184 |
| Total Lines | 1011 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like Rbl 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.
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 Rbl, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 18 | class Rbl extends \App\Base |
||
| 19 | { |
||
| 20 | /** |
||
| 21 | * Request statuses. |
||
| 22 | * |
||
| 23 | * @var array |
||
| 24 | */ |
||
| 25 | public const REQUEST_STATUS = [ |
||
| 26 | 0 => ['label' => 'LBL_FOR_VERIFICATION', 'icon' => 'fas fa-question'], |
||
| 27 | 1 => ['label' => 'LBL_ACCEPTED', 'icon' => 'fas fa-check text-success'], |
||
| 28 | 2 => ['label' => 'LBL_REJECTED', 'icon' => 'fas fa-times text-danger'], |
||
| 29 | 3 => ['label' => 'PLL_CANCELLED', 'icon' => 'fas fa-minus'], |
||
| 30 | 4 => ['label' => 'LBL_REPORTED', 'icon' => 'fas fa-paper-plane text-primary'], |
||
| 31 | ]; |
||
| 32 | /** |
||
| 33 | * List statuses. |
||
| 34 | * |
||
| 35 | * @var array |
||
| 36 | */ |
||
| 37 | public const LIST_STATUS = [ |
||
| 38 | 0 => ['label' => 'LBL_ACTIVE', 'icon' => 'fas fa-check text-success'], |
||
| 39 | 1 => ['label' => 'LBL_CANCELED', 'icon' => 'fas fa-times text-danger'], |
||
| 40 | ]; |
||
| 41 | /** |
||
| 42 | * List statuses. |
||
| 43 | * |
||
| 44 | * @var array |
||
| 45 | */ |
||
| 46 | public const LIST_TYPES = [ |
||
| 47 | 0 => ['label' => 'LBL_BLACK_LIST', 'icon' => 'fas fa-ban text-danger', 'alertColor' => '#ff555233', 'listColor' => '#ff555233'], |
||
| 48 | 1 => ['label' => 'LBL_WHITE_LIST', 'icon' => 'far fa-check-circle text-success', 'alertColor' => '#E1FFE3', 'listColor' => '#fff'], |
||
| 49 | 2 => ['label' => 'LBL_PUBLIC_BLACK_LIST', 'icon' => 'fas fa-ban text-danger', 'alertColor' => '#eaeaea', 'listColor' => '#ff555233'], |
||
| 50 | 3 => ['label' => 'LBL_PUBLIC_WHITE_LIST', 'icon' => 'far fa-check-circle text-success', 'alertColor' => '#E1FFE3', 'listColor' => '#fff'], |
||
| 51 | ]; |
||
| 52 | /** |
||
| 53 | * List categories. |
||
| 54 | * |
||
| 55 | * @var array |
||
| 56 | */ |
||
| 57 | public const LIST_CATEGORIES = [ |
||
| 58 | 'Black' => [ |
||
| 59 | '[SPAM] Single unwanted message' => 'LBL_SPAM_SINGLE_UNWANTED_MESSAGE', |
||
| 60 | '[SPAM] Mass unwanted message' => 'LBL_SPAM_MASS_UNWANTED_MESSAGE', |
||
| 61 | '[SPAM] Sending an unsolicited message repeatedly' => 'LBL_SPAM_SENDING_UNSOLICITED_MESSAGE_REPEATEDLY', |
||
| 62 | '[Fraud] Money scam' => 'LBL_FRAUD_MONEY_SCAM', |
||
| 63 | '[Fraud] Phishing' => 'LBL_FRAUD_PHISHING', |
||
| 64 | '[Fraud] An attempt to persuade people to buy a product or service' => 'LBL_FRAUD_ATTEMPT_TO_PERSUADE_PEOPLE_TO_BUY', |
||
| 65 | '[Security] An attempt to impersonate another person' => 'LBL_SECURITY_ATTEMPT_TO_IMPERSONATE_ANOTHER_PERSON', |
||
| 66 | '[Security] An attempt to persuade the recipient to open a resource from outside the organization' => 'LBL_SECURITY_ATTEMPT_TO_PERSUADE_FROM_ORGANIZATION', |
||
| 67 | '[Security] An attempt to persuade the recipient to open a resource inside the organization' => 'LBL_SECURITY_ATTEMPT_TO_PERSUADE_INSIDE_ORGANIZATION', |
||
| 68 | '[Security] Infrastructure and application scanning' => 'LBL_SECURITY_INFRASTRUCTURE_AND_APPLICATION_SCANNING', |
||
| 69 | '[Security] Attack on infrastructure or application' => 'LBL_SECURITY_ATTACK_INFRASTRUCTURE_OR_APPLICATION', |
||
| 70 | '[Security] Overloading infrastructure or application' => 'LBL_SECURITY_OVERLOADING_INFRASTRUCTURE_OR_APPLICATION', |
||
| 71 | '[Other] The message contains inappropriate words' => 'LBL_OTHER_MESSAGE_CONTAINS_INAPPROPRIATE_WORDS', |
||
| 72 | '[Other] The message contains inappropriate materials' => 'LBL_OTHER_MESSAGE_CONTAINS_INAPPROPRIATE_MATERIALS', |
||
| 73 | '[Other] Malicious message' => 'LBL_OTHER_MALICIOUS_MESSAGE', |
||
| 74 | ], |
||
| 75 | 'White' => [ |
||
| 76 | '[Whitelist] Trusted sender' => 'LBL_TRUSTED_SENDER', |
||
| 77 | ], |
||
| 78 | ]; |
||
| 79 | /** |
||
| 80 | * RLB black list type. |
||
| 81 | * |
||
| 82 | * @var int |
||
| 83 | */ |
||
| 84 | public const LIST_TYPE_BLACK_LIST = 0; |
||
| 85 | /** |
||
| 86 | * RLB white list type. |
||
| 87 | * |
||
| 88 | * @var int |
||
| 89 | */ |
||
| 90 | public const LIST_TYPE_WHITE_LIST = 1; |
||
| 91 | /** |
||
| 92 | * RLB public black list type. |
||
| 93 | * |
||
| 94 | * @var int |
||
| 95 | */ |
||
| 96 | public const LIST_TYPE_PUBLIC_BLACK_LIST = 2; |
||
| 97 | /** |
||
| 98 | * RLB public white list type. |
||
| 99 | * |
||
| 100 | * @var int |
||
| 101 | */ |
||
| 102 | public const LIST_TYPE_PUBLIC_WHITE_LIST = 3; |
||
| 103 | |||
| 104 | /** |
||
| 105 | * SPF statuses. |
||
| 106 | * |
||
| 107 | * @var array |
||
| 108 | */ |
||
| 109 | public const SPF = [ |
||
| 110 | 1 => ['label' => 'LBL_NONE', 'desc' => 'LBL_SPF_NONE_DESC', 'class' => 'badge-secondary', 'icon' => 'fas fa-question'], |
||
| 111 | 2 => ['label' => 'LBL_CORRECT', 'desc' => 'LBL_SPF_PASS_DESC', 'class' => 'badge-success', 'icon' => 'fas fa-check'], |
||
| 112 | 3 => ['label' => 'LBL_INCORRECT', 'desc' => 'LBL_SPF_FAIL_DESC', 'class' => 'badge-danger', 'icon' => 'fas fa-times'], |
||
| 113 | ]; |
||
| 114 | /** |
||
| 115 | * Check result: None, Neutral, TempError, PermError. |
||
| 116 | * |
||
| 117 | * @var int |
||
| 118 | */ |
||
| 119 | public const SPF_NONE = 1; |
||
| 120 | /** |
||
| 121 | * Check result: Pass (the SPF record stated that the IP address is authorized). |
||
| 122 | * |
||
| 123 | * @var int |
||
| 124 | */ |
||
| 125 | public const SPF_PASS = 2; |
||
| 126 | /** |
||
| 127 | * Check result: Fail, SoftFail. |
||
| 128 | * |
||
| 129 | * @var int |
||
| 130 | */ |
||
| 131 | public const SPF_FAIL = 3; |
||
| 132 | /** |
||
| 133 | * DKIM statuses. |
||
| 134 | * |
||
| 135 | * @var array |
||
| 136 | */ |
||
| 137 | public const DKIM = [ |
||
| 138 | 0 => ['label' => 'LBL_NONE', 'desc' => 'LBL_DKIM_NONE_DESC', 'class' => 'badge-secondary', 'icon' => 'fas fa-question'], |
||
| 139 | 1 => ['label' => 'LBL_CORRECT', 'desc' => 'LBL_DKIM_PASS_DESC', 'class' => 'badge-success', 'icon' => 'fas fa-check'], |
||
| 140 | 2 => ['label' => 'LBL_INCORRECT', 'desc' => 'LBL_DKIM_FAIL_DESC', 'class' => 'badge-danger', 'icon' => 'fas fa-times'], |
||
| 141 | ]; |
||
| 142 | /** |
||
| 143 | * DKIM header not found. |
||
| 144 | * |
||
| 145 | * @var int |
||
| 146 | */ |
||
| 147 | public const DKIM_NONE = 0; |
||
| 148 | /** |
||
| 149 | * DKIM header verified correctly. |
||
| 150 | * |
||
| 151 | * @var int |
||
| 152 | */ |
||
| 153 | public const DKIM_PASS = 1; |
||
| 154 | /** |
||
| 155 | * DKIM header verified incorrectly. |
||
| 156 | * |
||
| 157 | * @var int |
||
| 158 | */ |
||
| 159 | public const DKIM_FAIL = 2; |
||
| 160 | /** |
||
| 161 | * DMARC statuses. |
||
| 162 | * |
||
| 163 | * @var array |
||
| 164 | */ |
||
| 165 | public const DMARC = [ |
||
| 166 | 0 => ['label' => 'LBL_NONE', 'desc' => 'LBL_DMARC_NONE_DESC', 'class' => 'badge-secondary', 'icon' => 'fas fa-question'], |
||
| 167 | 1 => ['label' => 'LBL_CORRECT', 'desc' => 'LBL_DMARC_PASS_DESC', 'class' => 'badge-success', 'icon' => 'fas fa-check'], |
||
| 168 | 2 => ['label' => 'LBL_INCORRECT', 'desc' => 'LBL_DMARC_FAIL_DESC', 'class' => 'badge-danger', 'icon' => 'fas fa-times'], |
||
| 169 | ]; |
||
| 170 | /** |
||
| 171 | * DMARC header not found. |
||
| 172 | * |
||
| 173 | * @var int |
||
| 174 | */ |
||
| 175 | public const DMARC_NONE = 0; |
||
| 176 | /** |
||
| 177 | * DMARC header verified correctly. |
||
| 178 | * |
||
| 179 | * @var int |
||
| 180 | */ |
||
| 181 | public const DMARC_PASS = 1; |
||
| 182 | /** |
||
| 183 | * DMARC header verified incorrectly. |
||
| 184 | * |
||
| 185 | * @var int |
||
| 186 | */ |
||
| 187 | public const DMARC_FAIL = 2; |
||
| 188 | /** |
||
| 189 | * Message mail mime parser instance. |
||
| 190 | * |
||
| 191 | * @var \ZBateson\MailMimeParser\Message |
||
| 192 | */ |
||
| 193 | public $mailMimeParser; |
||
| 194 | /** |
||
| 195 | * Sender cache. |
||
| 196 | * |
||
| 197 | * @var array |
||
| 198 | */ |
||
| 199 | private $senderCache; |
||
| 200 | /** |
||
| 201 | * SPF cache. |
||
| 202 | * |
||
| 203 | * @var array |
||
| 204 | */ |
||
| 205 | private $spfCache; |
||
| 206 | /** |
||
| 207 | * DKIM cache. |
||
| 208 | * |
||
| 209 | * @var array |
||
| 210 | */ |
||
| 211 | private $dkimCache; |
||
| 212 | |||
| 213 | /** |
||
| 214 | * Function to get the instance of advanced permission record model. |
||
| 215 | * |
||
| 216 | * @param int $id |
||
| 217 | * |
||
| 218 | * @return \self instance, if exists |
||
|
|
|||
| 219 | */ |
||
| 220 | public static function getRequestById(int $id) |
||
| 221 | { |
||
| 222 | $query = (new \App\Db\Query())->from('s_#__mail_rbl_request')->where(['id' => $id]); |
||
| 223 | $row = $query->createCommand(\App\Db::getInstance('admin'))->queryOne(); |
||
| 224 | $instance = false; |
||
| 225 | if (false !== $row) { |
||
| 226 | $instance = new self(); |
||
| 227 | $instance->setData($row); |
||
| 228 | } |
||
| 229 | return $instance; |
||
| 230 | } |
||
| 231 | |||
| 232 | /** |
||
| 233 | * Create an empty object and initialize it with data. |
||
| 234 | * |
||
| 235 | * @param array $data |
||
| 236 | * |
||
| 237 | * @return self |
||
| 238 | */ |
||
| 239 | public static function getInstance(array $data): self |
||
| 240 | { |
||
| 241 | $instance = new self(); |
||
| 242 | $instance->setData($data); |
||
| 243 | $instance->mailMimeParser = \ZBateson\MailMimeParser\Message::from($instance->get('header'), false); |
||
| 244 | return $instance; |
||
| 245 | } |
||
| 246 | |||
| 247 | /** |
||
| 248 | * Parsing the email body or headers. |
||
| 249 | * |
||
| 250 | * @return void |
||
| 251 | */ |
||
| 252 | public function parse(): void |
||
| 253 | { |
||
| 254 | $this->mailMimeParser = \ZBateson\MailMimeParser\Message::from($this->has('rawBody') ? $this->get('rawBody') : $this->get('header') . "\r\n\r\n", false); |
||
| 255 | } |
||
| 256 | |||
| 257 | /** |
||
| 258 | * Get email from header by name. |
||
| 259 | * |
||
| 260 | * @param string $name |
||
| 261 | * |
||
| 262 | * @return string |
||
| 263 | */ |
||
| 264 | protected function getHeaderEmail(string $name): string |
||
| 265 | { |
||
| 266 | if ($header = $this->mailMimeParser->getHeader($name)) { |
||
| 267 | return $header->getEmail() ?: ''; |
||
| 268 | } |
||
| 269 | return ''; |
||
| 270 | } |
||
| 271 | |||
| 272 | /** |
||
| 273 | * Get received header. |
||
| 274 | * |
||
| 275 | * @return array |
||
| 276 | */ |
||
| 277 | public function getReceived(): array |
||
| 278 | { |
||
| 279 | $rows = []; |
||
| 280 | foreach ($this->mailMimeParser->getAllHeadersByName('Received') as $key => $received) { |
||
| 281 | $row = ['key' => $key, 'fromIP' => $received->getFromAddress() ?? '']; |
||
| 282 | if ($received->getFromName()) { |
||
| 283 | $row['fromName'] = $received->getFromName(); |
||
| 284 | } |
||
| 285 | if ($received->getFromHostname()) { |
||
| 286 | $row['fromName'] .= PHP_EOL . '(' . $received->getFromHostname() . ')'; |
||
| 287 | } |
||
| 288 | if ($received->getByName()) { |
||
| 289 | $row['byName'] = $received->getByName(); |
||
| 290 | } |
||
| 291 | if ($received->getByHostname()) { |
||
| 292 | $row['byName'] .= PHP_EOL . '(' . $received->getByHostname() . ')'; |
||
| 293 | } |
||
| 294 | if ($received->getByAddress()) { |
||
| 295 | $row['byIP'] = $received->getByAddress(); |
||
| 296 | } |
||
| 297 | if ($received->getValueFor('with')) { |
||
| 298 | $row['extraWith'] = $received->getValueFor('with'); |
||
| 299 | } |
||
| 300 | if ($received->getComments()) { |
||
| 301 | $row['extraComments'] = preg_replace('/\s+/', ' ', trim(implode(' | ', $received->getComments()))); |
||
| 302 | } |
||
| 303 | if ($received->getDateTime()) { |
||
| 304 | $row['dateTime'] = $received->getDateTime()->format('Y-m-d H:i:s'); |
||
| 305 | } |
||
| 306 | $rows[] = $row; |
||
| 307 | } |
||
| 308 | return array_reverse($rows); |
||
| 309 | } |
||
| 310 | |||
| 311 | /** |
||
| 312 | * Get sender details. |
||
| 313 | * |
||
| 314 | * @return array |
||
| 315 | */ |
||
| 316 | public function getSender(): array |
||
| 317 | { |
||
| 318 | if (isset($this->senderCache)) { |
||
| 319 | return $this->senderCache; |
||
| 320 | } |
||
| 321 | $first = $row = []; |
||
| 322 | foreach ($this->mailMimeParser->getAllHeadersByName('Received') as $key => $received) { |
||
| 323 | if ($received->getFromName() && $received->getByName() && $received->getFromName() !== $received->getByName()) { |
||
| 324 | $fromDomain = $this->getDomain($received->getFromName()); |
||
| 325 | $byDomain = $this->getDomain($received->getByName()); |
||
| 326 | if (!($fromIp = $received->getFromAddress())) { |
||
| 327 | if (!($fromIp = $this->findIpByName($received, 'from'))) { |
||
| 328 | $fromIp = $this->getIpByName($received->getFromName(), $received->getFromHostname()); |
||
| 329 | } |
||
| 330 | } |
||
| 331 | if (!($byIp = $received->getByAddress())) { |
||
| 332 | if (!($byIp = $this->findIpByName($received, 'by'))) { |
||
| 333 | $byIp = $this->getIpByName($received->getByName(), $received->getByHostname()); |
||
| 334 | } |
||
| 335 | } |
||
| 336 | if ($fromIp && $byIp && $fromIp !== $byIp && ((!$fromDomain && !$byDomain) || $fromDomain !== $byDomain)) { |
||
| 337 | $row['ip'] = $fromIp; |
||
| 338 | $row['key'] = $key; |
||
| 339 | $row['from'] = $received->getFromName(); |
||
| 340 | $row['by'] = $received->getByName(); |
||
| 341 | break; |
||
| 342 | } |
||
| 343 | if (empty($first)) { |
||
| 344 | $first = [ |
||
| 345 | 'ip' => $fromIp, |
||
| 346 | 'key' => $key, |
||
| 347 | 'from' => $received->getFromName(), |
||
| 348 | 'by' => $received->getByName(), |
||
| 349 | ]; |
||
| 350 | } |
||
| 351 | } |
||
| 352 | if (!empty($byIp)) { |
||
| 353 | $row['ip'] = $byIp; |
||
| 354 | } elseif ($received->getByAddress()) { |
||
| 355 | $row['ip'] = $received->getByAddress(); |
||
| 356 | } |
||
| 357 | } |
||
| 358 | if (!isset($row['key'])) { |
||
| 359 | if ($first) { |
||
| 360 | $row = $first; |
||
| 361 | } else { |
||
| 362 | $row['key'] = false; |
||
| 363 | } |
||
| 364 | } |
||
| 365 | return $this->senderCache = $row; |
||
| 366 | } |
||
| 367 | |||
| 368 | /** |
||
| 369 | * Get domain from URL. |
||
| 370 | * |
||
| 371 | * @param string $url |
||
| 372 | * |
||
| 373 | * @return string |
||
| 374 | */ |
||
| 375 | public function getDomain(string $url): string |
||
| 376 | { |
||
| 377 | if (']' === substr($url, -1) || '[' === substr($url, 0, 1)) { |
||
| 378 | $url = rtrim(ltrim($url, '['), ']'); |
||
| 379 | } |
||
| 380 | if (filter_var($url, FILTER_VALIDATE_IP)) { |
||
| 381 | return $url; |
||
| 382 | } |
||
| 383 | $domains = explode('.', $url, substr_count($url, '.')); |
||
| 384 | return end($domains); |
||
| 385 | } |
||
| 386 | |||
| 387 | /** |
||
| 388 | * Find mail ip address. |
||
| 389 | * |
||
| 390 | * @param \ZBateson\MailMimeParser\Header\ReceivedHeader $received |
||
| 391 | * @param string $type |
||
| 392 | * |
||
| 393 | * @return string |
||
| 394 | */ |
||
| 395 | public function findIpByName(\ZBateson\MailMimeParser\Header\ReceivedHeader $received, string $type): string |
||
| 396 | { |
||
| 397 | $value = $received->getValueFor($type); |
||
| 398 | $pattern = '~\[(IPv[64])?([a-f\d\.\:]+)\]~i'; |
||
| 399 | if (preg_match($pattern, $value, $matches)) { |
||
| 400 | if (!empty($matches[2])) { |
||
| 401 | return $matches[2]; |
||
| 402 | } |
||
| 403 | } |
||
| 404 | $lastReceivedPart = null; |
||
| 405 | foreach ($received->getParts() as $part) { |
||
| 406 | if ($part instanceof \ZBateson\MailMimeParser\Header\Part\ReceivedPart) { |
||
| 407 | $lastReceivedPart = $part->getName(); |
||
| 408 | } elseif ($part instanceof \ZBateson\MailMimeParser\Header\Part\CommentPart) { |
||
| 409 | if ($lastReceivedPart === $type) { |
||
| 410 | if (preg_match($pattern, $part->getComment(), $matches)) { |
||
| 411 | if (!empty($matches[2])) { |
||
| 412 | return $matches[2]; |
||
| 413 | } |
||
| 414 | } |
||
| 415 | } |
||
| 416 | } |
||
| 417 | } |
||
| 418 | return ''; |
||
| 419 | } |
||
| 420 | |||
| 421 | /** |
||
| 422 | * Get mail ip address by hostname or ehloName. |
||
| 423 | * |
||
| 424 | * @param string $fromName |
||
| 425 | * @param ?string $hostName |
||
| 426 | * |
||
| 427 | * @return string |
||
| 428 | */ |
||
| 429 | public function getIpByName(string $fromName, ?string $hostName = null): string |
||
| 430 | { |
||
| 431 | if (']' === substr($fromName, -1) || '[' === substr($fromName, 0, 1)) { |
||
| 432 | $fromName = rtrim(ltrim($fromName, '['), ']'); |
||
| 433 | } |
||
| 434 | if (filter_var($fromName, FILTER_VALIDATE_IP)) { |
||
| 435 | return $fromName; |
||
| 436 | } |
||
| 437 | if (0 === stripos($hostName, 'helo=')) { |
||
| 438 | $hostName = substr($hostName, 5); |
||
| 439 | if ($ip = \App\RequestUtil::getIpByName($hostName)) { |
||
| 440 | return $ip; |
||
| 441 | } |
||
| 442 | } |
||
| 443 | if ($ip = \App\RequestUtil::getIpByName($fromName)) { |
||
| 444 | return $ip; |
||
| 445 | } |
||
| 446 | return ''; |
||
| 447 | } |
||
| 448 | |||
| 449 | /** |
||
| 450 | * Get senders. |
||
| 451 | * |
||
| 452 | * @return string |
||
| 453 | */ |
||
| 454 | public function getSenders(): array |
||
| 455 | { |
||
| 456 | $senders = [ |
||
| 457 | 'From' => $this->mailMimeParser->getHeaderValue('From'), |
||
| 458 | ]; |
||
| 459 | if ($returnPath = $this->mailMimeParser->getHeaderValue('Return-Path')) { |
||
| 460 | $senders['Return-Path'] = $returnPath; |
||
| 461 | } |
||
| 462 | if ($sender = $this->mailMimeParser->getHeaderValue('Sender')) { |
||
| 463 | $senders['Sender'] = $sender; |
||
| 464 | } |
||
| 465 | if ($sender = $this->mailMimeParser->getHeaderValue('X-Sender')) { |
||
| 466 | $senders['X-Sender'] = $sender; |
||
| 467 | } |
||
| 468 | return $senders; |
||
| 469 | } |
||
| 470 | |||
| 471 | /** |
||
| 472 | * Verify sender email address. |
||
| 473 | * |
||
| 474 | * @return array |
||
| 475 | */ |
||
| 476 | public function verifySender(): array |
||
| 477 | { |
||
| 478 | $from = $this->getHeaderEmail('from'); |
||
| 479 | if (!$from) { |
||
| 480 | return ['status' => true, 'info' => '']; |
||
| 481 | } |
||
| 482 | $status = true; |
||
| 483 | $info = ''; |
||
| 484 | if ($returnPath = $this->getHeaderEmail('Return-Path')) { |
||
| 485 | if (0 === stripos($returnPath, 'SRS') && ($separator = substr($returnPath, 4, 1)) !== '') { |
||
| 486 | $returnPathSrs = ''; |
||
| 487 | $parts = explode($separator, $returnPath); |
||
| 488 | $mail = explode('@', array_pop($parts)); |
||
| 489 | if (isset($mail[1])) { |
||
| 490 | $last = array_pop($parts); |
||
| 491 | $returnPathSrs = "{$mail[0]}@{$last}"; |
||
| 492 | } |
||
| 493 | $status = $from === $returnPathSrs; |
||
| 494 | } else { |
||
| 495 | $status = $from === $returnPath; |
||
| 496 | } |
||
| 497 | if (!$status) { |
||
| 498 | $info .= "From: {$from} <> Return-Path: {$returnPath}" . PHP_EOL; |
||
| 499 | } |
||
| 500 | } |
||
| 501 | if ($status && ($sender = $this->getHeaderEmail('Sender'))) { |
||
| 502 | $status = $from === $sender; |
||
| 503 | if (!$status) { |
||
| 504 | $info .= "From: {$from} <> Sender: {$sender}" . PHP_EOL; |
||
| 505 | } |
||
| 506 | } |
||
| 507 | return ['status' => $status, 'info' => $info]; |
||
| 508 | } |
||
| 509 | |||
| 510 | /** |
||
| 511 | * Verify SPF (Sender Policy Framework) for Authorizing Use of Domains in Email. |
||
| 512 | * |
||
| 513 | * @see https://tools.ietf.org/html/rfc7208 |
||
| 514 | * |
||
| 515 | * @return array |
||
| 516 | */ |
||
| 517 | public function verifySpf(): array |
||
| 518 | { |
||
| 519 | if (isset($this->spfCache)) { |
||
| 520 | return $this->spfCache; |
||
| 521 | } |
||
| 522 | $returnPath = $this->getHeaderEmail('Return-Path'); |
||
| 523 | $sender = $this->getSender(); |
||
| 524 | $return = ['status' => self::SPF_NONE]; |
||
| 525 | if ($email = $this->getHeaderEmail('from')) { |
||
| 526 | $return['domain'] = explode('@', $email)[1]; |
||
| 527 | } |
||
| 528 | if (isset($sender['ip'])) { |
||
| 529 | $cacheKey = "{$sender['ip']}-{$returnPath}"; |
||
| 530 | if (\App\Cache::has('RBL:verifySpf', $cacheKey)) { |
||
| 531 | $status = \App\Cache::get('RBL:verifySpf', $cacheKey); |
||
| 532 | } else { |
||
| 533 | $status = null; |
||
| 534 | try { |
||
| 535 | $environment = new \SPFLib\Check\Environment($sender['ip'], '', $returnPath); |
||
| 536 | switch ((new \SPFLib\Checker())->check($environment, \SPFLib\Checker::FLAG_CHECK_MAILFROADDRESS)->getCode()) { |
||
| 537 | case \SPFLib\Check\Result::CODE_PASS: |
||
| 538 | $status = self::SPF_PASS; |
||
| 539 | break; |
||
| 540 | case \SPFLib\Check\Result::CODE_FAIL: |
||
| 541 | case \SPFLib\Check\Result::CODE_SOFTFAIL: |
||
| 542 | $status = self::SPF_FAIL; |
||
| 543 | break; |
||
| 544 | } |
||
| 545 | } catch (\Throwable $e) { |
||
| 546 | \App\Log::warning($e->getMessage(), __NAMESPACE__); |
||
| 547 | } |
||
| 548 | \App\Cache::save('RBL:verifySpf', $cacheKey, $status, \App\Cache::LONG); |
||
| 549 | } |
||
| 550 | if (isset($status)) { |
||
| 551 | $return['status'] = $status; |
||
| 552 | } |
||
| 553 | } |
||
| 554 | return $this->spfCache = $return + self::SPF[$return['status']]; |
||
| 555 | } |
||
| 556 | |||
| 557 | /** |
||
| 558 | * Verify DKIM (DomainKeys Identified Mail). |
||
| 559 | * |
||
| 560 | * @see https://tools.ietf.org/html/rfc4871 |
||
| 561 | * |
||
| 562 | * @return array |
||
| 563 | */ |
||
| 564 | public function verifyDkim(): array |
||
| 565 | { |
||
| 566 | if (isset($this->dkimCache)) { |
||
| 567 | return $this->dkimCache; |
||
| 568 | } |
||
| 569 | $content = $this->has('rawBody') ? $this->get('rawBody') : $this->get('header') . "\r\n\r\n"; |
||
| 570 | $status = self::DKIM_NONE; |
||
| 571 | $logs = ''; |
||
| 572 | if ($this->mailMimeParser->getHeader('DKIM-Signature')) { |
||
| 573 | try { |
||
| 574 | $validate = (new \PHPMailer\DKIMValidator\Validator($content))->validate(); |
||
| 575 | $status = ((1 === \count($validate)) && (1 === \count($validate[0])) && ('SUCCESS' === $validate[0][0]['status'])) ? self::DKIM_PASS : self::DKIM_FAIL; |
||
| 576 | foreach ($validate as $rows) { |
||
| 577 | foreach ($rows as $row) { |
||
| 578 | $logs .= "[{$row['status']}] {$row['reason']}\n"; |
||
| 579 | } |
||
| 580 | } |
||
| 581 | } catch (\Throwable $e) { |
||
| 582 | \App\Log::warning($e->getMessage(), __NAMESPACE__); |
||
| 583 | } |
||
| 584 | } |
||
| 585 | return $this->dkimCache = \App\Utils::merge(['status' => $status, 'logs' => trim($logs)], self::DKIM[$status]); |
||
| 586 | } |
||
| 587 | |||
| 588 | /** |
||
| 589 | * Verify DMARC (Domain-based Message Authentication, Reporting and Conformance). |
||
| 590 | * |
||
| 591 | * @return array |
||
| 592 | */ |
||
| 593 | public function verifyDmarc(): array |
||
| 594 | { |
||
| 595 | $fromDomain = explode('@', $this->getHeaderEmail('from'))[1] ?? ''; |
||
| 596 | $status = self::DMARC_NONE; |
||
| 597 | if (empty($fromDomain) || !($dmarcRecord = $this->getDmarcRecord($fromDomain))) { |
||
| 598 | return ['status' => $status, 'logs' => \App\Language::translateArgs('LBL_NO_DMARC_DNS', 'Settings:MailRbl', $fromDomain)] + self::DMARC[$status]; |
||
| 599 | } |
||
| 600 | $logs = ''; |
||
| 601 | if ($this->mailMimeParser->getHeader('DKIM-Signature')) { |
||
| 602 | $verifyDmarcDkim = $this->verifyDmarcDkim($fromDomain, $dmarcRecord['adkim']); |
||
| 603 | $status = $verifyDmarcDkim['status'] && self::DKIM_PASS === $this->verifyDkim()['status'] ? self::DMARC_PASS : self::DMARC_FAIL; |
||
| 604 | if (!$status) { |
||
| 605 | $logs = $verifyDmarcDkim['log']; |
||
| 606 | } |
||
| 607 | } |
||
| 608 | if (self::DMARC_FAIL !== $status) { |
||
| 609 | $verifyDmarcSpf = $this->verifyDmarcSpf($fromDomain, $dmarcRecord['aspf']); |
||
| 610 | if (null === $verifyDmarcSpf['status']) { |
||
| 611 | $logs = \App\Language::translate('LBL_NO_DMARC_FROM', 'Settings:MailRbl'); |
||
| 612 | } else { |
||
| 613 | $status = $verifyDmarcSpf['status'] && self::SPF_PASS === $this->verifySpf()['status'] ? self::DMARC_PASS : self::DMARC_FAIL; |
||
| 614 | if (!$verifyDmarcSpf['status']) { |
||
| 615 | $logs = $verifyDmarcSpf['log']; |
||
| 616 | } |
||
| 617 | } |
||
| 618 | } |
||
| 619 | return ['status' => $status, 'logs' => trim($logs)] + self::DMARC[$status]; |
||
| 620 | } |
||
| 621 | |||
| 622 | /** |
||
| 623 | * Get DMARC TXT record. |
||
| 624 | * |
||
| 625 | * @param string $domain |
||
| 626 | * |
||
| 627 | * @return array |
||
| 628 | */ |
||
| 629 | public function getDmarcRecord(string $domain): array |
||
| 630 | { |
||
| 631 | if (\App\Cache::has('RBL:getDmarcRecord', $domain)) { |
||
| 632 | return \App\Cache::get('RBL:getDmarcRecord', $domain); |
||
| 633 | } |
||
| 634 | $dns = dns_get_record('_dmarc.' . $domain, DNS_TXT); |
||
| 635 | if (!$dns) { |
||
| 636 | return []; |
||
| 637 | } |
||
| 638 | $dkimParams = []; |
||
| 639 | foreach ($dns as $key) { |
||
| 640 | if (preg_match('/^v=dmarc(.*)/i', $key['txt'])) { |
||
| 641 | $dkimParams = self::parseHeaderParams($key['txt']); |
||
| 642 | break; |
||
| 643 | } |
||
| 644 | } |
||
| 645 | if (empty($dkimParams['adkim'])) { |
||
| 646 | $dkimParams['adkim'] = 'r'; |
||
| 647 | } |
||
| 648 | if (empty($dkimParams['aspf'])) { |
||
| 649 | $dkimParams['aspf'] = 'r'; |
||
| 650 | } |
||
| 651 | \App\Cache::save('RBL:getDmarcRecord', $domain, $dkimParams, \App\Cache::LONG); |
||
| 652 | return $dkimParams; |
||
| 653 | } |
||
| 654 | |||
| 655 | /** |
||
| 656 | * Verify DMARC ADKIM Tag. |
||
| 657 | * |
||
| 658 | * @param string $fromDomain |
||
| 659 | * @param string $adkim |
||
| 660 | * |
||
| 661 | * @return array |
||
| 662 | */ |
||
| 663 | private function verifyDmarcDkim(string $fromDomain, string $adkim): array |
||
| 664 | { |
||
| 665 | $dkimDomain = self::parseHeaderParams($this->mailMimeParser->getHeaderValue('DKIM-Signature'))['d']; |
||
| 666 | $status = $fromDomain === $dkimDomain; |
||
| 667 | if ($status || 's' === $adkim) { |
||
| 668 | return ['status' => $status, 'log' => ($status ? '' : "From: $fromDomain | DKIM domain: $dkimDomain")]; |
||
| 669 | } |
||
| 670 | $status = (mb_strlen($fromDomain) - mb_strlen('.' . $dkimDomain)) === strpos($fromDomain, '.' . $dkimDomain) || (mb_strlen($dkimDomain) - mb_strlen('.' . $fromDomain)) === strpos($dkimDomain, '.' . $fromDomain); |
||
| 671 | return ['status' => $status, 'log' => ($status ? '' : "From: $fromDomain | DKIM domain: $dkimDomain")]; |
||
| 672 | } |
||
| 673 | |||
| 674 | /** |
||
| 675 | * Verify DMARC ASPF Tag. |
||
| 676 | * |
||
| 677 | * @param string $fromDomain |
||
| 678 | * @param string $aspf |
||
| 679 | * |
||
| 680 | * @return array |
||
| 681 | */ |
||
| 682 | private function verifyDmarcSpf(string $fromDomain, string $aspf): array |
||
| 683 | { |
||
| 684 | $mailFrom = ''; |
||
| 685 | if ($returnPath = $this->getHeaderEmail('Return-Path')) { |
||
| 686 | $mailFrom = explode('@', $returnPath)[1]; |
||
| 687 | } |
||
| 688 | if (!$mailFrom && !($mailFrom = $this->getSender()['from'] ?? '')) { |
||
| 689 | return ['status' => null]; |
||
| 690 | } |
||
| 691 | $status = $fromDomain === $mailFrom; |
||
| 692 | if ($status || 's' === $aspf) { |
||
| 693 | return ['status' => $status, 'log' => ($status ? '' : "RFC5321.MailFrom domain: $mailFrom | RFC5322.From domain: $fromDomain")]; |
||
| 694 | } |
||
| 695 | $logs = ''; |
||
| 696 | $status = (mb_strlen($mailFrom) - mb_strlen('.' . $fromDomain)) === strpos($mailFrom, '.' . $fromDomain); |
||
| 697 | if (!$status) { |
||
| 698 | $logs = "RFC5321.MailFrom domain: $mailFrom | RFC5322.From domain: $fromDomain \n"; |
||
| 699 | if ($this->mailMimeParser->getHeader('DKIM-Signature')) { |
||
| 700 | $dkimDomain = self::parseHeaderParams($this->mailMimeParser->getHeaderValue('DKIM-Signature'))['d']; |
||
| 701 | $status = (mb_strlen($dkimDomain) - mb_strlen('.' . $fromDomain)) === strpos($dkimDomain, '.' . $fromDomain); |
||
| 702 | if (!$status) { |
||
| 703 | $logs .= "DKIM domain: $dkimDomain | RFC5322.From domain: $fromDomain"; |
||
| 704 | } |
||
| 705 | } |
||
| 706 | } |
||
| 707 | return ['status' => $status, 'log' => trim($logs)]; |
||
| 708 | } |
||
| 709 | |||
| 710 | /** |
||
| 711 | * Update list by request id. |
||
| 712 | * |
||
| 713 | * @param int $record |
||
| 714 | * |
||
| 715 | * @return int |
||
| 716 | */ |
||
| 717 | public function updateList(int $record): int |
||
| 718 | { |
||
| 719 | $sender = $this->getSender(); |
||
| 720 | $return = 0; |
||
| 721 | if (!empty($sender['ip'])) { |
||
| 722 | $dbCommand = \App\Db::getInstance('admin')->createCommand(); |
||
| 723 | $id = false; |
||
| 724 | if ($rows = self::findIp($sender['ip'])) { |
||
| 725 | foreach ($rows as $row) { |
||
| 726 | if ((self::LIST_TYPE_BLACK_LIST !== (int) $row['type']) && (self::LIST_TYPE_PUBLIC_BLACK_LIST !== (int) $row['type'])) { |
||
| 727 | $id = $row['id']; |
||
| 728 | break; |
||
| 729 | } |
||
| 730 | } |
||
| 731 | } |
||
| 732 | if ($id) { |
||
| 733 | $dbCommand->update('s_#__mail_rbl_list', [ |
||
| 734 | 'status' => 0, |
||
| 735 | 'type' => $this->get('type'), |
||
| 736 | 'request' => $record, |
||
| 737 | ], ['id' => $id])->execute(); |
||
| 738 | } else { |
||
| 739 | $dbCommand->insert('s_#__mail_rbl_list', [ |
||
| 740 | 'ip' => $sender['ip'], |
||
| 741 | 'status' => 0, |
||
| 742 | 'type' => $this->get('type'), |
||
| 743 | 'request' => $record, |
||
| 744 | 'source' => '', |
||
| 745 | ])->execute(); |
||
| 746 | } |
||
| 747 | \App\Cache::delete('MailRblIpColor', $sender['ip']); |
||
| 748 | \App\Cache::delete('MailRblList', $sender['ip']); |
||
| 749 | $return = 2; |
||
| 750 | if (\Config\Components\Mail::$rcListSendReportAutomatically ?? false) { |
||
| 751 | self::sendReport(['id' => $record]); |
||
| 752 | $return = 3; |
||
| 753 | } |
||
| 754 | } |
||
| 755 | return $return; |
||
| 756 | } |
||
| 757 | |||
| 758 | /** |
||
| 759 | * Get color by ips. |
||
| 760 | * |
||
| 761 | * @param array $ips |
||
| 762 | * |
||
| 763 | * @return array |
||
| 764 | */ |
||
| 765 | public static function getColorByIps(array $ips): array |
||
| 790 | } |
||
| 791 | |||
| 792 | /** |
||
| 793 | * Find ip in list. |
||
| 794 | * |
||
| 795 | * @param string $ip |
||
| 796 | * @param bool $onlyActive |
||
| 797 | * |
||
| 798 | * @return array |
||
| 799 | */ |
||
| 800 | public static function findIp(string $ip, $onlyActive = false): array |
||
| 813 | } |
||
| 814 | |||
| 815 | /** |
||
| 816 | * Get color by list. |
||
| 817 | * |
||
| 818 | * @param string $ip |
||
| 819 | * @param array $rows |
||
| 820 | * |
||
| 821 | * @return string |
||
| 822 | */ |
||
| 823 | public static function getColorByList(string $ip, array $rows): string |
||
| 824 | { |
||
| 825 | if (\App\Cache::has('MailRblIpColor', $ip)) { |
||
| 826 | return \App\Cache::get('MailRblIpColor', $ip); |
||
| 827 | } |
||
| 828 | $color = ''; |
||
| 829 | foreach ($rows as $row) { |
||
| 830 | if (1 !== (int) $row['status']) { |
||
| 831 | $color = self::LIST_TYPES[$row['type']]['listColor']; |
||
| 832 | break; |
||
| 833 | } |
||
| 834 | } |
||
| 835 | \App\Cache::save('MailRblIpColor', $ip, $color, \App\Cache::LONG); |
||
| 836 | return $color; |
||
| 837 | } |
||
| 838 | |||
| 839 | /** |
||
| 840 | * Parse header params. |
||
| 841 | * |
||
| 842 | * @param string $string |
||
| 843 | * |
||
| 844 | * @return array |
||
| 845 | */ |
||
| 846 | public static function parseHeaderParams(string $string): array |
||
| 847 | { |
||
| 848 | $params = []; |
||
| 849 | foreach (explode(';', rtrim(preg_replace('/\s+/', '', $string), ';')) as $param) { |
||
| 850 | [$tagName, $tagValue] = explode('=', trim($param), 2); |
||
| 851 | if ('' !== $tagName) { |
||
| 852 | $params[$tagName] = $tagValue; |
||
| 853 | } |
||
| 854 | } |
||
| 855 | return $params; |
||
| 856 | } |
||
| 857 | |||
| 858 | /** |
||
| 859 | * Add report. |
||
| 860 | * |
||
| 861 | * @param array $data |
||
| 862 | * |
||
| 863 | * @return string |
||
| 864 | */ |
||
| 865 | public static function addReport(array $data): string |
||
| 920 | } |
||
| 921 | |||
| 922 | /** |
||
| 923 | * Send report. |
||
| 924 | * |
||
| 925 | * @param array $data |
||
| 926 | * |
||
| 927 | * @return array |
||
| 928 | */ |
||
| 929 | public static function sendReport(array $data): array |
||
| 966 | } |
||
| 967 | |||
| 968 | /** |
||
| 969 | * Get IP list from public RBL. |
||
| 970 | * |
||
| 971 | * @param int $type |
||
| 972 | * |
||
| 973 | * @return array |
||
| 974 | */ |
||
| 975 | public static function getPublicList(int $type): array |
||
| 976 | { |
||
| 977 | if (!\App\RequestUtil::isNetConnection()) { |
||
| 978 | \App\Log::warning('ERR_NO_INTERNET_CONNECTION', __METHOD__); |
||
| 979 | return []; |
||
| 980 | } |
||
| 981 | $url = 'https://soc.yetiforce.com/list/' . (self::LIST_TYPE_PUBLIC_BLACK_LIST === $type ? 'black' : 'white'); |
||
| 982 | \App\Log::beginProfile("POST|Rbl::sendReport|{$url}", __NAMESPACE__); |
||
| 983 | $response = (new \GuzzleHttp\Client(\App\RequestHttp::getOptions()))->get($url, [ |
||
| 984 | 'http_errors' => false, |
||
| 985 | 'headers' => [ |
||
| 986 | 'crm-ik' => \App\YetiForce\Register::getInstanceKey(), |
||
| 987 | ], |
||
| 988 | ]); |
||
| 989 | \App\Log::endProfile("POST|Rbl::sendReport|{$url}", __NAMESPACE__); |
||
| 990 | $list = []; |
||
| 991 | if (200 === $response->getStatusCode()) { |
||
| 992 | $list = \App\Json::decode($response->getBody()->getContents()) ?? []; |
||
| 993 | } else { |
||
| 994 | $body = \App\Json::decode($response->getBody()->getContents()); |
||
| 995 | \App\Log::warning($response->getReasonPhrase() . ' | ' . $body['error']['message'], __METHOD__); |
||
| 996 | } |
||
| 997 | return $list; |
||
| 998 | } |
||
| 999 | |||
| 1000 | /** |
||
| 1001 | * Public list synchronization. |
||
| 1002 | * |
||
| 1003 | * @param int $type |
||
| 1004 | * |
||
| 1005 | * @return void |
||
| 1006 | */ |
||
| 1007 | public static function sync(int $type): void |
||
| 1029 | } |
||
| 1030 | } |
||
| 1031 | } |
||
| 1032 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths