YetiForceCompany/YetiForceCRM

View on GitHub
app/Mail/Rbl.php

Summary

Maintainability
F
1 wk
Test Coverage
F
0%
<?php
/**
 * Mail RBL file.
 *
 * @package App
 *
 * @copyright YetiForce S.A.
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
 * @author    Mariusz Krzaczkowski <m.krzaczkowski@yetiforce.com>
 * @author    Radosław Skrzypczak <r.skrzypczak@yetiforce.com>
 */

namespace App\Mail;

/**
 * Mail RBL class.
 */
class Rbl extends \App\Base
{
    /**
     * Request statuses.
     *
     * @var array
     */
    public const REQUEST_STATUS = [
        0 => ['label' => 'LBL_FOR_VERIFICATION', 'icon' => 'fas fa-question'],
        1 => ['label' => 'LBL_ACCEPTED', 'icon' => 'fas fa-check text-success'],
        2 => ['label' => 'LBL_REJECTED', 'icon' => 'fas fa-times text-danger'],
        3 => ['label' => 'PLL_CANCELLED', 'icon' => 'fas fa-minus'],
        4 => ['label' => 'LBL_REPORTED', 'icon' => 'fas fa-paper-plane text-primary'],
    ];
    /**
     * List statuses.
     *
     * @var array
     */
    public const LIST_STATUS = [
        0 => ['label' => 'LBL_ACTIVE', 'icon' => 'fas fa-check text-success'],
        1 => ['label' => 'LBL_CANCELED', 'icon' => 'fas fa-times text-danger'],
    ];
    /**
     * List statuses.
     *
     * @var array
     */
    public const LIST_TYPES = [
        0 => ['label' => 'LBL_BLACK_LIST', 'icon' => 'fas fa-ban text-danger', 'alertColor' => '#ff555233', 'listColor' => '#ff555233'],
        1 => ['label' => 'LBL_WHITE_LIST', 'icon' => 'far fa-check-circle text-success', 'alertColor' => '#E1FFE3', 'listColor' => '#fff'],
        2 => ['label' => 'LBL_PUBLIC_BLACK_LIST', 'icon' => 'fas fa-ban text-danger', 'alertColor' => '#eaeaea', 'listColor' => '#ff555233'],
        3 => ['label' => 'LBL_PUBLIC_WHITE_LIST', 'icon' => 'far fa-check-circle text-success', 'alertColor' => '#E1FFE3', 'listColor' => '#fff'],
    ];
    /**
     * List categories.
     *
     * @var array
     */
    public const LIST_CATEGORIES = [
        'Black' => [
            '[SPAM] Single unwanted message' => 'LBL_SPAM_SINGLE_UNWANTED_MESSAGE',
            '[SPAM] Mass unwanted message' => 'LBL_SPAM_MASS_UNWANTED_MESSAGE',
            '[SPAM] Sending an unsolicited message repeatedly' => 'LBL_SPAM_SENDING_UNSOLICITED_MESSAGE_REPEATEDLY',
            '[Fraud] Money scam' => 'LBL_FRAUD_MONEY_SCAM',
            '[Fraud] Phishing' => 'LBL_FRAUD_PHISHING',
            '[Fraud] An attempt to persuade people to buy a product or service' => 'LBL_FRAUD_ATTEMPT_TO_PERSUADE_PEOPLE_TO_BUY',
            '[Security] An attempt to impersonate another person' => 'LBL_SECURITY_ATTEMPT_TO_IMPERSONATE_ANOTHER_PERSON',
            '[Security] An attempt to persuade the recipient to open a resource from outside the organization' => 'LBL_SECURITY_ATTEMPT_TO_PERSUADE_FROM_ORGANIZATION',
            '[Security] An attempt to persuade the recipient to open a resource inside the organization' => 'LBL_SECURITY_ATTEMPT_TO_PERSUADE_INSIDE_ORGANIZATION',
            '[Security] Infrastructure and application scanning' => 'LBL_SECURITY_INFRASTRUCTURE_AND_APPLICATION_SCANNING',
            '[Security] Attack on infrastructure or application' => 'LBL_SECURITY_ATTACK_INFRASTRUCTURE_OR_APPLICATION',
            '[Security] Overloading infrastructure or application' => 'LBL_SECURITY_OVERLOADING_INFRASTRUCTURE_OR_APPLICATION',
            '[Other] The message contains inappropriate words' => 'LBL_OTHER_MESSAGE_CONTAINS_INAPPROPRIATE_WORDS',
            '[Other] The message contains inappropriate materials' => 'LBL_OTHER_MESSAGE_CONTAINS_INAPPROPRIATE_MATERIALS',
            '[Other] Malicious message' => 'LBL_OTHER_MALICIOUS_MESSAGE',
        ],
        'White' => [
            '[Whitelist] Trusted sender' => 'LBL_TRUSTED_SENDER',
        ],
    ];
    /**
     * RLB black list type.
     *
     * @var int
     */
    public const LIST_TYPE_BLACK_LIST = 0;
    /**
     * RLB white list type.
     *
     * @var int
     */
    public const LIST_TYPE_WHITE_LIST = 1;
    /**
     * RLB public black list type.
     *
     * @var int
     */
    public const LIST_TYPE_PUBLIC_BLACK_LIST = 2;
    /**
     * RLB public white list type.
     *
     * @var int
     */
    public const LIST_TYPE_PUBLIC_WHITE_LIST = 3;

    /**
     * SPF statuses.
     *
     * @var array
     */
    public const SPF = [
        1 => ['label' => 'LBL_NONE', 'desc' => 'LBL_SPF_NONE_DESC', 'class' => 'badge-secondary', 'icon' => 'fas fa-question'],
        2 => ['label' => 'LBL_CORRECT', 'desc' => 'LBL_SPF_PASS_DESC', 'class' => 'badge-success', 'icon' => 'fas fa-check'],
        3 => ['label' => 'LBL_INCORRECT', 'desc' => 'LBL_SPF_FAIL_DESC', 'class' => 'badge-danger', 'icon' => 'fas fa-times'],
    ];
    /**
     * Check result: None, Neutral, TempError, PermError.
     *
     * @var int
     */
    public const SPF_NONE = 1;
    /**
     * Check result: Pass (the SPF record stated that the IP address is authorized).
     *
     * @var int
     */
    public const SPF_PASS = 2;
    /**
     * Check result: Fail, SoftFail.
     *
     * @var int
     */
    public const SPF_FAIL = 3;
    /**
     * DKIM statuses.
     *
     * @var array
     */
    public const DKIM = [
        0 => ['label' => 'LBL_NONE', 'desc' => 'LBL_DKIM_NONE_DESC', 'class' => 'badge-secondary', 'icon' => 'fas fa-question'],
        1 => ['label' => 'LBL_CORRECT', 'desc' => 'LBL_DKIM_PASS_DESC', 'class' => 'badge-success', 'icon' => 'fas fa-check'],
        2 => ['label' => 'LBL_INCORRECT', 'desc' => 'LBL_DKIM_FAIL_DESC', 'class' => 'badge-danger', 'icon' => 'fas fa-times'],
    ];
    /**
     * DKIM header not found.
     *
     * @var int
     */
    public const DKIM_NONE = 0;
    /**
     * DKIM header verified correctly.
     *
     * @var int
     */
    public const DKIM_PASS = 1;
    /**
     * DKIM header verified incorrectly.
     *
     * @var int
     */
    public const DKIM_FAIL = 2;
    /**
     * DMARC statuses.
     *
     * @var array
     */
    public const DMARC = [
        0 => ['label' => 'LBL_NONE', 'desc' => 'LBL_DMARC_NONE_DESC', 'class' => 'badge-secondary', 'icon' => 'fas fa-question'],
        1 => ['label' => 'LBL_CORRECT', 'desc' => 'LBL_DMARC_PASS_DESC', 'class' => 'badge-success', 'icon' => 'fas fa-check'],
        2 => ['label' => 'LBL_INCORRECT', 'desc' => 'LBL_DMARC_FAIL_DESC', 'class' => 'badge-danger', 'icon' => 'fas fa-times'],
    ];
    /**
     * DMARC header not found.
     *
     * @var int
     */
    public const DMARC_NONE = 0;
    /**
     * DMARC header verified correctly.
     *
     * @var int
     */
    public const DMARC_PASS = 1;
    /**
     * DMARC header verified incorrectly.
     *
     * @var int
     */
    public const DMARC_FAIL = 2;
    /**
     * Message mail mime parser instance.
     *
     * @var \ZBateson\MailMimeParser\Message
     */
    public $mailMimeParser;
    /**
     * Sender cache.
     *
     * @var array
     */
    private $senderCache;
    /**
     * SPF cache.
     *
     * @var array
     */
    private $spfCache;
    /**
     * DKIM cache.
     *
     * @var array
     */
    private $dkimCache;

    /**
     * Function to get the instance of advanced permission record model.
     *
     * @param int $id
     *
     * @return \self instance, if exists
     */
    public static function getRequestById(int $id)
    {
        $query = (new \App\Db\Query())->from('s_#__mail_rbl_request')->where(['id' => $id]);
        $row = $query->createCommand(\App\Db::getInstance('admin'))->queryOne();
        $instance = false;
        if (false !== $row) {
            $instance = new self();
            $instance->setData($row);
        }
        return $instance;
    }

    /**
     * Create an empty object and initialize it with data.
     *
     * @param array $data
     *
     * @return self
     */
    public static function getInstance(array $data): self
    {
        $instance = new self();
        $instance->setData($data);
        $instance->mailMimeParser = \ZBateson\MailMimeParser\Message::from($instance->get('header'), false);
        return $instance;
    }

    /**
     * Parsing the email body or headers.
     *
     * @return void
     */
    public function parse(): void
    {
        $this->mailMimeParser = \ZBateson\MailMimeParser\Message::from($this->has('rawBody') ? $this->get('rawBody') : $this->get('header') . "\r\n\r\n", false);
    }

    /**
     * Get email from header by name.
     *
     * @param string $name
     *
     * @return string
     */
    protected function getHeaderEmail(string $name): string
    {
        if ($header = $this->mailMimeParser->getHeader($name)) {
            return $header->getEmail() ?: '';
        }
        return '';
    }

    /**
     * Get received header.
     *
     * @return array
     */
    public function getReceived(): array
    {
        $rows = [];
        foreach ($this->mailMimeParser->getAllHeadersByName('Received') as $key => $received) {
            $row = ['key' => $key, 'fromIP' => $received->getFromAddress() ?? ''];
            if ($received->getFromName()) {
                $row['fromName'] = $received->getFromName();
            }
            if ($received->getFromHostname()) {
                $row['fromName'] .= PHP_EOL . '(' . $received->getFromHostname() . ')';
            }
            if ($received->getByName()) {
                $row['byName'] = $received->getByName();
            }
            if ($received->getByHostname()) {
                $row['byName'] .= PHP_EOL . '(' . $received->getByHostname() . ')';
            }
            if ($received->getByAddress()) {
                $row['byIP'] = $received->getByAddress();
            }
            if ($received->getValueFor('with')) {
                $row['extraWith'] = $received->getValueFor('with');
            }
            if ($received->getComments()) {
                $row['extraComments'] = preg_replace('/\s+/', ' ', trim(implode(' | ', $received->getComments())));
            }
            if ($received->getDateTime()) {
                $row['dateTime'] = $received->getDateTime()->format('Y-m-d H:i:s');
            }
            $rows[] = $row;
        }
        return array_reverse($rows);
    }

    /**
     * Get sender details.
     *
     * @return array
     */
    public function getSender(): array
    {
        if (isset($this->senderCache)) {
            return $this->senderCache;
        }
        $first = $row = [];
        foreach ($this->mailMimeParser->getAllHeadersByName('Received') as $key => $received) {
            if ($received->getFromName() && $received->getByName() && $received->getFromName() !== $received->getByName()) {
                $fromDomain = $this->getDomain($received->getFromName());
                $byDomain = $this->getDomain($received->getByName());
                if (!($fromIp = $received->getFromAddress())) {
                    if (!($fromIp = $this->findIpByName($received, 'from'))) {
                        $fromIp = $this->getIpByName($received->getFromName(), $received->getFromHostname());
                    }
                }
                if (!($byIp = $received->getByAddress())) {
                    if (!($byIp = $this->findIpByName($received, 'by'))) {
                        $byIp = $this->getIpByName($received->getByName(), $received->getByHostname());
                    }
                }
                if ($fromIp && $byIp && $fromIp !== $byIp && ((!$fromDomain && !$byDomain) || $fromDomain !== $byDomain)) {
                    $row['ip'] = $fromIp;
                    $row['key'] = $key;
                    $row['from'] = $received->getFromName();
                    $row['by'] = $received->getByName();
                    break;
                }
                if (empty($first)) {
                    $first = [
                        'ip' => $fromIp,
                        'key' => $key,
                        'from' => $received->getFromName(),
                        'by' => $received->getByName(),
                    ];
                }
            }
            if (!empty($byIp)) {
                $row['ip'] = $byIp;
            } elseif ($received->getByAddress()) {
                $row['ip'] = $received->getByAddress();
            }
        }
        if (!isset($row['key'])) {
            if ($first) {
                $row = $first;
            } else {
                $row['key'] = false;
            }
        }
        return $this->senderCache = $row;
    }

    /**
     * Get domain from URL.
     *
     * @param string $url
     *
     * @return string
     */
    public function getDomain(string $url): string
    {
        if (']' === substr($url, -1) || '[' === substr($url, 0, 1)) {
            $url = rtrim(ltrim($url, '['), ']');
        }
        if (filter_var($url, FILTER_VALIDATE_IP)) {
            return $url;
        }
        $domains = explode('.', $url, substr_count($url, '.'));
        return end($domains);
    }

    /**
     * Find mail ip address.
     *
     * @param \ZBateson\MailMimeParser\Header\ReceivedHeader $received
     * @param string                                         $type
     *
     * @return string
     */
    public function findIpByName(\ZBateson\MailMimeParser\Header\ReceivedHeader $received, string $type): string
    {
        $value = $received->getValueFor($type);
        $pattern = '~\[(IPv[64])?([a-f\d\.\:]+)\]~i';
        if (preg_match($pattern, $value, $matches)) {
            if (!empty($matches[2])) {
                return $matches[2];
            }
        }
        $lastReceivedPart = null;
        foreach ($received->getParts() as $part) {
            if ($part instanceof \ZBateson\MailMimeParser\Header\Part\ReceivedPart) {
                $lastReceivedPart = $part->getName();
            } elseif ($part instanceof \ZBateson\MailMimeParser\Header\Part\CommentPart) {
                if ($lastReceivedPart === $type) {
                    if (preg_match($pattern, $part->getComment(), $matches)) {
                        if (!empty($matches[2])) {
                            return $matches[2];
                        }
                    }
                }
            }
        }
        return '';
    }

    /**
     * Get mail ip address by hostname or ehloName.
     *
     * @param string  $fromName
     * @param ?string $hostName
     *
     * @return string
     */
    public function getIpByName(string $fromName, ?string $hostName = null): string
    {
        if (']' === substr($fromName, -1) || '[' === substr($fromName, 0, 1)) {
            $fromName = rtrim(ltrim($fromName, '['), ']');
        }
        if (filter_var($fromName, FILTER_VALIDATE_IP)) {
            return $fromName;
        }
        if (0 === stripos($hostName, 'helo=')) {
            $hostName = substr($hostName, 5);
            if ($ip = \App\RequestUtil::getIpByName($hostName)) {
                return $ip;
            }
        }
        if ($ip = \App\RequestUtil::getIpByName($fromName)) {
            return $ip;
        }
        return '';
    }

    /**
     * Get senders.
     *
     * @return string
     */
    public function getSenders(): array
    {
        $senders = [
            'From' => $this->mailMimeParser->getHeaderValue('From'),
        ];
        if ($returnPath = $this->mailMimeParser->getHeaderValue('Return-Path')) {
            $senders['Return-Path'] = $returnPath;
        }
        if ($sender = $this->mailMimeParser->getHeaderValue('Sender')) {
            $senders['Sender'] = $sender;
        }
        if ($sender = $this->mailMimeParser->getHeaderValue('X-Sender')) {
            $senders['X-Sender'] = $sender;
        }
        return $senders;
    }

    /**
     * Verify sender email address.
     *
     * @return array
     */
    public function verifySender(): array
    {
        $from = $this->getHeaderEmail('from');
        if (!$from) {
            return ['status' => true, 'info' => ''];
        }
        $status = true;
        $info = '';
        if ($returnPath = $this->getHeaderEmail('Return-Path')) {
            if (0 === stripos($returnPath, 'SRS') && ($separator = substr($returnPath, 4, 1)) !== '') {
                $returnPathSrs = '';
                $parts = explode($separator, $returnPath);
                $mail = explode('@', array_pop($parts));
                if (isset($mail[1])) {
                    $last = array_pop($parts);
                    $returnPathSrs = "{$mail[0]}@{$last}";
                }
                $status = $from === $returnPathSrs;
            } else {
                $status = $from === $returnPath;
            }
            if (!$status) {
                $info .= "From: {$from} <> Return-Path: {$returnPath}" . PHP_EOL;
            }
        }
        if ($status && ($sender = $this->getHeaderEmail('Sender'))) {
            $status = $from === $sender;
            if (!$status) {
                $info .= "From: {$from} <> Sender: {$sender}" . PHP_EOL;
            }
        }
        return ['status' => $status, 'info' => $info];
    }

    /**
     * Verify SPF (Sender Policy Framework) for Authorizing Use of Domains in Email.
     *
     * @see https://tools.ietf.org/html/rfc7208
     *
     * @return array
     */
    public function verifySpf(): array
    {
        if (isset($this->spfCache)) {
            return $this->spfCache;
        }
        $returnPath = $this->getHeaderEmail('Return-Path');
        $sender = $this->getSender();
        $return = ['status' => self::SPF_NONE];
        if ($email = $this->getHeaderEmail('from')) {
            $return['domain'] = explode('@', $email)[1];
        }
        if (isset($sender['ip'])) {
            $cacheKey = "{$sender['ip']}-{$returnPath}";
            if (\App\Cache::has('RBL:verifySpf', $cacheKey)) {
                $status = \App\Cache::get('RBL:verifySpf', $cacheKey);
            } else {
                $status = null;
                try {
                    $environment = new \SPFLib\Check\Environment($sender['ip'], '', $returnPath);
                    switch ((new \SPFLib\Checker())->check($environment, \SPFLib\Checker::FLAG_CHECK_MAILFROADDRESS)->getCode()) {
                        case \SPFLib\Check\Result::CODE_PASS:
                            $status = self::SPF_PASS;
                            break;
                        case \SPFLib\Check\Result::CODE_FAIL:
                        case \SPFLib\Check\Result::CODE_SOFTFAIL:
                            $status = self::SPF_FAIL;
                            break;
                    }
                } catch (\Throwable $e) {
                    \App\Log::warning($e->getMessage(), __NAMESPACE__);
                }
                \App\Cache::save('RBL:verifySpf', $cacheKey, $status, \App\Cache::LONG);
            }
            if (isset($status)) {
                $return['status'] = $status;
            }
        }
        return $this->spfCache = $return + self::SPF[$return['status']];
    }

    /**
     * Verify DKIM (DomainKeys Identified Mail).
     *
     * @see https://tools.ietf.org/html/rfc4871
     *
     * @return array
     */
    public function verifyDkim(): array
    {
        if (isset($this->dkimCache)) {
            return $this->dkimCache;
        }
        $content = $this->has('rawBody') ? $this->get('rawBody') : $this->get('header') . "\r\n\r\n";
        $status = self::DKIM_NONE;
        $logs = '';
        if ($this->mailMimeParser->getHeader('DKIM-Signature')) {
            try {
                $validate = (new \PHPMailer\DKIMValidator\Validator($content))->validate();
                $status = ((1 === \count($validate)) && (1 === \count($validate[0])) && ('SUCCESS' === $validate[0][0]['status'])) ? self::DKIM_PASS : self::DKIM_FAIL;
                foreach ($validate as $rows) {
                    foreach ($rows as $row) {
                        $logs .= "[{$row['status']}] {$row['reason']}\n";
                    }
                }
            } catch (\Throwable $e) {
                \App\Log::warning($e->getMessage(), __NAMESPACE__);
            }
        }
        return $this->dkimCache = \App\Utils::merge(['status' => $status, 'logs' => trim($logs)], self::DKIM[$status]);
    }

    /**
     * Verify DMARC (Domain-based Message Authentication, Reporting and Conformance).
     *
     * @return array
     */
    public function verifyDmarc(): array
    {
        $fromDomain = explode('@', $this->getHeaderEmail('from'))[1] ?? '';
        $status = self::DMARC_NONE;
        if (empty($fromDomain) || !($dmarcRecord = $this->getDmarcRecord($fromDomain))) {
            return ['status' => $status, 'logs' => \App\Language::translateArgs('LBL_NO_DMARC_DNS', 'Settings:MailRbl', $fromDomain)] + self::DMARC[$status];
        }
        $logs = '';
        if ($this->mailMimeParser->getHeader('DKIM-Signature')) {
            $verifyDmarcDkim = $this->verifyDmarcDkim($fromDomain, $dmarcRecord['adkim']);
            $status = $verifyDmarcDkim['status'] && self::DKIM_PASS === $this->verifyDkim()['status'] ? self::DMARC_PASS : self::DMARC_FAIL;
            if (!$status) {
                $logs = $verifyDmarcDkim['log'];
            }
        }
        if (self::DMARC_FAIL !== $status) {
            $verifyDmarcSpf = $this->verifyDmarcSpf($fromDomain, $dmarcRecord['aspf']);
            if (null === $verifyDmarcSpf['status']) {
                $logs = \App\Language::translate('LBL_NO_DMARC_FROM', 'Settings:MailRbl');
            } else {
                $status = $verifyDmarcSpf['status'] && self::SPF_PASS === $this->verifySpf()['status'] ? self::DMARC_PASS : self::DMARC_FAIL;
                if (!$verifyDmarcSpf['status']) {
                    $logs = $verifyDmarcSpf['log'];
                }
            }
        }
        return ['status' => $status, 'logs' => trim($logs)] + self::DMARC[$status];
    }

    /**
     * Get DMARC TXT record.
     *
     * @param string $domain
     *
     * @return array
     */
    public function getDmarcRecord(string $domain): array
    {
        if (\App\Cache::has('RBL:getDmarcRecord', $domain)) {
            return \App\Cache::get('RBL:getDmarcRecord', $domain);
        }
        $dns = dns_get_record('_dmarc.' . $domain, DNS_TXT);
        if (!$dns) {
            return [];
        }
        $dkimParams = [];
        foreach ($dns as $key) {
            if (preg_match('/^v=dmarc(.*)/i', $key['txt'])) {
                $dkimParams = self::parseHeaderParams($key['txt']);
                break;
            }
        }
        if (empty($dkimParams['adkim'])) {
            $dkimParams['adkim'] = 'r';
        }
        if (empty($dkimParams['aspf'])) {
            $dkimParams['aspf'] = 'r';
        }
        \App\Cache::save('RBL:getDmarcRecord', $domain, $dkimParams, \App\Cache::LONG);
        return $dkimParams;
    }

    /**
     * Verify DMARC ADKIM Tag.
     *
     * @param string $fromDomain
     * @param string $adkim
     *
     * @return array
     */
    private function verifyDmarcDkim(string $fromDomain, string $adkim): array
    {
        $dkimDomain = self::parseHeaderParams($this->mailMimeParser->getHeaderValue('DKIM-Signature'))['d'];
        $status = $fromDomain === $dkimDomain;
        if ($status || 's' === $adkim) {
            return ['status' => $status, 'log' => ($status ? '' : "From: $fromDomain | DKIM domain: $dkimDomain")];
        }
        $status = (mb_strlen($fromDomain) - mb_strlen('.' . $dkimDomain)) === strpos($fromDomain, '.' . $dkimDomain) || (mb_strlen($dkimDomain) - mb_strlen('.' . $fromDomain)) === strpos($dkimDomain, '.' . $fromDomain);
        return ['status' => $status, 'log' => ($status ? '' : "From: $fromDomain | DKIM domain: $dkimDomain")];
    }

    /**
     * Verify DMARC ASPF Tag.
     *
     * @param string $fromDomain
     * @param string $aspf
     *
     * @return array
     */
    private function verifyDmarcSpf(string $fromDomain, string $aspf): array
    {
        $mailFrom = '';
        if ($returnPath = $this->getHeaderEmail('Return-Path')) {
            $mailFrom = explode('@', $returnPath)[1];
        }
        if (!$mailFrom && !($mailFrom = $this->getSender()['from'] ?? '')) {
            return ['status' => null];
        }
        $status = $fromDomain === $mailFrom;
        if ($status || 's' === $aspf) {
            return ['status' => $status, 'log' => ($status ? '' : "RFC5321.MailFrom domain: $mailFrom | RFC5322.From domain: $fromDomain")];
        }
        $logs = '';
        $status = (mb_strlen($mailFrom) - mb_strlen('.' . $fromDomain)) === strpos($mailFrom, '.' . $fromDomain);
        if (!$status) {
            $logs = "RFC5321.MailFrom domain: $mailFrom | RFC5322.From domain: $fromDomain \n";
            if ($this->mailMimeParser->getHeader('DKIM-Signature')) {
                $dkimDomain = self::parseHeaderParams($this->mailMimeParser->getHeaderValue('DKIM-Signature'))['d'];
                $status = (mb_strlen($dkimDomain) - mb_strlen('.' . $fromDomain)) === strpos($dkimDomain, '.' . $fromDomain);
                if (!$status) {
                    $logs .= "DKIM domain: $dkimDomain | RFC5322.From domain: $fromDomain";
                }
            }
        }
        return ['status' => $status, 'log' => trim($logs)];
    }

    /**
     * Update list by request id.
     *
     * @param int $record
     *
     * @return int
     */
    public function updateList(int $record): int
    {
        $sender = $this->getSender();
        $return = 0;
        if (!empty($sender['ip'])) {
            $dbCommand = \App\Db::getInstance('admin')->createCommand();
            $id = false;
            if ($rows = self::findIp($sender['ip'])) {
                foreach ($rows as $row) {
                    if ((self::LIST_TYPE_BLACK_LIST !== (int) $row['type']) && (self::LIST_TYPE_PUBLIC_BLACK_LIST !== (int) $row['type'])) {
                        $id = $row['id'];
                        break;
                    }
                }
            }
            if ($id) {
                $dbCommand->update('s_#__mail_rbl_list', [
                    'status' => 0,
                    'type' => $this->get('type'),
                    'request' => $record,
                ], ['id' => $id])->execute();
            } else {
                $dbCommand->insert('s_#__mail_rbl_list', [
                    'ip' => $sender['ip'],
                    'status' => 0,
                    'type' => $this->get('type'),
                    'request' => $record,
                    'source' => '',
                ])->execute();
            }
            \App\Cache::delete('MailRblIpColor', $sender['ip']);
            \App\Cache::delete('MailRblList', $sender['ip']);
            $return = 2;
            if (\Config\Components\Mail::$rcListSendReportAutomatically ?? false) {
                self::sendReport(['id' => $record]);
                $return = 3;
            }
        }
        return $return;
    }

    /**
     * Get color by ips.
     *
     * @param array $ips
     *
     * @return array
     */
    public static function getColorByIps(array $ips): array
    {
        $return = $find = [];
        foreach ($ips as $uid => $ip) {
            if (\App\Cache::has('MailRblIpColor', $ip)) {
                $return[$uid] = \App\Cache::get('MailRblIpColor', $ip);
            } else {
                $find[$ip][] = $uid;
            }
        }
        if ($find) {
            $list = [];
            $dataReader = (new \App\Db\Query())->select(['id', 'ip', 'status', 'source', 'type'])->from('s_#__mail_rbl_list')->where(['ip' => array_keys($find)])
                ->createCommand(\App\Db::getInstance('admin'))->query();
            while ($row = $dataReader->read()) {
                $list[$row['ip']][] = $row;
            }
            foreach ($list as $ip => $rows) {
                $color = self::getColorByList($ip, $rows);
                foreach ($find[$ip] as $uid) {
                    $return[$uid] = $color;
                }
            }
        }
        return $return;
    }

    /**
     * Find ip in list.
     *
     * @param string $ip
     * @param bool   $onlyActive
     *
     * @return array
     */
    public static function findIp(string $ip, $onlyActive = false): array
    {
        $cacheName = "$ip|$onlyActive";
        if (\App\Cache::has('MailRblList', $cacheName)) {
            return \App\Cache::get('MailRblList', $cacheName);
        }
        $query = (new \App\Db\Query())->from('s_#__mail_rbl_list')->where(['ip' => $ip])->orderBy(['type' => SORT_ASC]);
        if ($onlyActive) {
            $query->andWhere(['status' => 0]);
        }
        $rows = $query->all(\App\Db::getInstance('admin'));
        \App\Cache::save('MailRblList', $cacheName, $rows, \App\Cache::LONG);
        return $rows;
    }

    /**
     * Get color by list.
     *
     * @param string $ip
     * @param array  $rows
     *
     * @return string
     */
    public static function getColorByList(string $ip, array $rows): string
    {
        if (\App\Cache::has('MailRblIpColor', $ip)) {
            return \App\Cache::get('MailRblIpColor', $ip);
        }
        $color = '';
        foreach ($rows as $row) {
            if (1 !== (int) $row['status']) {
                $color = self::LIST_TYPES[$row['type']]['listColor'];
                break;
            }
        }
        \App\Cache::save('MailRblIpColor', $ip, $color, \App\Cache::LONG);
        return $color;
    }

    /**
     * Parse header params.
     *
     * @param string $string
     *
     * @return array
     */
    public static function parseHeaderParams(string $string): array
    {
        $params = [];
        foreach (explode(';', rtrim(preg_replace('/\s+/', '', $string), ';')) as $param) {
            [$tagName, $tagValue] = explode('=', trim($param), 2);
            if ('' !== $tagName) {
                $params[$tagName] = $tagValue;
            }
        }
        return $params;
    }

    /**
     * Add report.
     *
     * @param array $data
     *
     * @return string
     */
    public static function addReport(array $data): string
    {
        $status = 0;
        if (\Config\Components\Mail::$rcListAcceptAutomatically ?? false) {
            $status = 1;
        }
        $db = \App\Db::getInstance('admin');
        $dbCommand = $db->createCommand();
        $dbCommand->insert('s_#__mail_rbl_request', [
            'status' => $status,
            'datetime' => date('Y-m-d H:i:s'),
            'user' => \App\User::getCurrentUserId(),
            'type' => $data['type'],
            'header' => $data['header'],
            'body' => $data['body'] ?? null,
        ])->execute();
        $record = $db->getLastInsertID();
        $return = 'LBL_RC_ERROR_RBL_REPORT';
        if ($record) {
            $return = 'LBL_RC_ADDED_RBL_REPORT_LOCAL';
            if ($status) {
                $rblRecord = self::getRequestById($record);
                $rblRecord->parse();
                $sender = $rblRecord->getSender();
                if (empty($sender['ip'])) {
                    $dbCommand->update('s_#__mail_rbl_request', ['status' => 0], ['id' => $record])->execute();
                    $return = 'LBL_NO_IP_ADDRESS';
                } else {
                    $blacklist = 0 == $rblRecord->get('type');
                    $skipUpdate = false;
                    if ($rows = self::findIp($sender['ip'])) {
                        foreach ($rows as $row) {
                            if (0 == $row['status']) {
                                if ($blacklist && (self::LIST_TYPE_WHITE_LIST == $row['type'] || self::LIST_TYPE_PUBLIC_WHITE_LIST == $row['type'])) {
                                    $return = 'LBL_RC_ADDED_RBL_REPORT_IP_WHITE';
                                    $skipUpdate = true;
                                    break;
                                }
                                if (!$blacklist && (self::LIST_TYPE_WHITE_LIST == $row['type'] || self::LIST_TYPE_PUBLIC_WHITE_LIST == $row['type'])) {
                                    $return = 'LBL_RC_ADDED_RBL_REPORT_IP_BLACK';
                                    $skipUpdate = true;
                                    break;
                                }
                            }
                        }
                    }
                    if ($skipUpdate) {
                        $dbCommand->update('s_#__mail_rbl_request', ['status' => 0], ['id' => $record])->execute();
                    } elseif (3 === $rblRecord->updateList($record)) {
                        $return = 'LBL_RC_ADDED_RBL_REPORT_PUBLIC';
                    }
                }
            }
        }
        return $return;
    }

    /**
     * Send report.
     *
     * @param array $data
     *
     * @return array
     */
    public static function sendReport(array $data): array
    {
        if (!\App\RequestUtil::isNetConnection()) {
            return ['status' => false, 'message' => \App\Language::translate('ERR_NO_INTERNET_CONNECTION', 'Other:Exceptions')];
        }
        $id = $data['id'];
        unset($data['id']);
        $recordModel = self::getRequestById($id);
        $recordModel->parse();
        $data['type'] = $recordModel->get('type') ? 'White' : 'Black';
        if (empty($data['category'])) {
            $data['category'] = '[SPAM] Single unwanted message';
        }
        $url = 'https://soc.yetiforce.com/api/Application';
        \App\Log::beginProfile("POST|Rbl::sendReport|{$url}", __NAMESPACE__);
        $response = (new \GuzzleHttp\Client(\App\RequestHttp::getOptions()))->post($url, [
            'http_errors' => false,
            'headers' => [
                'crm-ik' => \App\YetiForce\Register::getInstanceKey(),
            ],
            'json' => array_merge($data, [
                'ik' => \App\YetiForce\Register::getInstanceKey(),
                'ip' => $recordModel->getSender()['ip'] ?? '-',
                'header' => $recordModel->get('header'),
                'body' => $recordModel->get('body'),
            ]),
        ]);
        \App\Log::endProfile("POST|Rbl::sendReport|{$url}", __NAMESPACE__);
        $body = \App\Json::decode($response->getBody()->getContents());
        if (200 == $response->getStatusCode() && 'ok' === $body['result']) {
            \App\Db::getInstance('admin')->createCommand()->update('s_#__mail_rbl_request', [
                'status' => 4,
            ], ['id' => $id])->execute();
            return ['status' => true];
        }
        \App\Log::warning($response->getReasonPhrase() . ' | ' . ($body['error']['message'] ?? $body['result']), __METHOD__);
        return ['status' => false, 'message' => ($body['error']['message'] ?? $body['result'])];
    }

    /**
     * Get IP list from public RBL.
     *
     * @param int $type
     *
     * @return array
     */
    public static function getPublicList(int $type): array
    {
        if (!\App\RequestUtil::isNetConnection()) {
            \App\Log::warning('ERR_NO_INTERNET_CONNECTION', __METHOD__);
            return [];
        }
        $url = 'https://soc.yetiforce.com/list/' . (self::LIST_TYPE_PUBLIC_BLACK_LIST === $type ? 'black' : 'white');
        \App\Log::beginProfile("POST|Rbl::sendReport|{$url}", __NAMESPACE__);
        $response = (new \GuzzleHttp\Client(\App\RequestHttp::getOptions()))->get($url, [
            'http_errors' => false,
            'headers' => [
                'crm-ik' => \App\YetiForce\Register::getInstanceKey(),
            ],
        ]);
        \App\Log::endProfile("POST|Rbl::sendReport|{$url}", __NAMESPACE__);
        $list = [];
        if (200 === $response->getStatusCode()) {
            $list = \App\Json::decode($response->getBody()->getContents()) ?? [];
        } else {
            $body = \App\Json::decode($response->getBody()->getContents());
            \App\Log::warning($response->getReasonPhrase() . ' | ' . $body['error']['message'], __METHOD__);
        }
        return $list;
    }

    /**
     * Public list synchronization.
     *
     * @param int $type
     *
     * @return void
     */
    public static function sync(int $type): void
    {
        if (!\App\RequestUtil::isNetConnection()) {
            \App\Log::warning('ERR_NO_INTERNET_CONNECTION', __METHOD__);
            return;
        }
        $public = self::getPublicList($type);
        $publicKeys = array_keys($public);
        $db = \App\Db::getInstance('admin');
        $dbCommand = $db->createCommand();
        $query = (new \App\Db\Query())->select(['ip', 'source', 'id'])->from('s_#__mail_rbl_list')->where(['type' => $type]);
        $rows = $query->createCommand($db)->queryAllByGroup(1);
        $keys = array_keys($rows);
        foreach (array_chunk(array_diff($publicKeys, $keys), 50, true) as $chunk) {
            $insertData = [];
            foreach ($chunk as $ip) {
                $insertData[] = [$ip, 0, $type, $public[$ip]['source'], $public[$ip]['comment']];
            }
            $dbCommand->batchInsert('s_#__mail_rbl_list', ['ip', 'status', 'type', 'source', 'comment'], $insertData)->execute();
        }
        foreach (array_diff($keys, $publicKeys) as $ip) {
            $dbCommand->delete('s_#__mail_rbl_list', ['id' => $rows[$ip]['id']])->execute();
        }
    }
}