HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/app/Model/SendingDomain.php
<?php

/**
 * SendingDomain class.
 *
 * Model class for sending domains
 *
 * LICENSE: This product includes software developed at
 * the Acelle Co., Ltd. (http://acellemail.com/).
 *
 * @category   MVC Model
 *
 * @author     N. Pham <[email protected]>
 * @author     L. Pham <[email protected]>
 * @copyright  Acelle Co., Ltd
 * @license    Acelle Co., Ltd
 *
 * @version    1.0
 *
 * @link       http://acellemail.com
 */

namespace Acelle\Model;

use Illuminate\Database\Eloquent\Model;
use Acelle\Library\MtaSync;
use Acelle\Library\Traits\HasUid;
use Validator;
use DB;
use Exception;
use Mika56\SPFCheck\SPFCheck;
use Acelle\Library\Domain;

use function Acelle\Helpers\spfcheck;

class SendingDomain extends Model
{
    use HasUid;

    public const STATUS_ACTIVE = 'active';
    public const STATUS_INACTIVE = 'inactive';

    // The regex below also supports Cyrillic
    public const VALIDATION_REGEXP = '/^(?!\-)(?:(?:[a-zA-Z\d][a-zA-Z\d\-]{0,61})?[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}$/i';

    // The regex below does not supports Cyrillic
    // public const VALIDATION_REGEXP = '/^([a-z0-9A-Z]+(-[a-z0-9A-Z]+)*\.)+[a-zA-Z]{2,}$/';


    // The following global app settings are used by domain
    // + spf_host
    // + spf_record
    // + verification_hostname

    /**
     * Associations.
     *
     * @var object | collect
     */
    public function customer()
    {
        return $this->belongsTo('Acelle\Model\Customer');
    }

    public function admin()
    {
        return $this->belongsTo('Acelle\Model\Admin');
    }

    public function sendingServer()
    {
        return $this->belongsTo('Acelle\Model\SendingServer');
    }

    public function scopeActive($query)
    {
        return $query->where('status', self::STATUS_ACTIVE);
    }

    public function scopeInactive($query)
    {
        return $query->where('status', self::STATUS_INACTIVE);
    }

    public function scopeBySendingServer($query, $server)
    {
        return $query->where('sending_server_id', $server->id);
    }

    /**
     * Filter items.
     *
     * @return collect
     */
    public function scopeSearch($query, $keyword)
    {
        if (!empty($keyword)) {
            $query->where('sending_domains.name', 'like', '%'.$keyword.'%');
        }

        return $query;
    }

    /**
     * Items per page.
     *
     * @var array
     */
    public static $itemsPerPage = 25;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'signing_enabled',
    ];

    /**
     * Get validation rules.
     *
     * @return object
     */
    public static function rules()
    {
        return [
            'name' => ['required', 'regex:'.self::VALIDATION_REGEXP],
        ];
    }

    public function getDomain()
    {
        if (!empty($this->dkim_selector)) {
            $selector = $this->dkim_selector;
        } else {
            $selector = Setting::get('dkim_selector');
        }

        $domain = new Domain(
            $this->name,
            $selector,
            $this->dkim_public,
            $this->dkim_private,
        );

        return $domain;
    }

    /**
     * Generate SPF: allow sending through the host's server (IP address)
     * See more at: http://www.openspf.org/SPF_Record_Syntax.
     */
    public function getSpf()
    {
        $spfRecord = Setting::get('spf_record');
        $spfHost = Setting::get('spf_host');

        if ($spfRecord && is_null($spfHost)) {
            throw new \Exception('`spf_record` is present but `spf_host` is empty');
        }

        if (empty($spfRecord)) {
            return null;
        }

        return $spfRecord;
    }

    /**
     * Get quoted SPF.
     */
    public function getQuotedSpf()
    {
        // If a "spf" setting is available, it will take precedence
        return Setting::get('spf') ?: sprintf('%s', doublequote($this->getSpf()));
    }

    /**
     * Retrieve the VERIFICATION_TXT_NAME value which is used as TXT name.
     */
    public function getVerificationHostName()
    {
        return Setting::get('verification_hostname');
    }

    /**
     * Retrieve the full verification hostname (including domain name).
     */
    public function getFullVerificationHostName()
    {
        return "{$this->getVerificationHostName()}.{$this->getDnsHostName()}";
    }

    /**
     * Get DNS host name.
     */
    public function getDnsHostName()
    {
        return "{$this->name}.";
    }

    /**
     * Generate the verification token.
     */
    public function generateIdentityToken()
    {
        return base64_encode(md5(trim('SALT!'.$this->name)));
    }

    /**
     * Create the private and public key.
     *
     * @var bool
     */
    public function generateDkimKeys()
    {
        $config = array(
            'digest_alg' => 'sha256',
            'private_key_bits' => 1024,
            'private_key_type' => OPENSSL_KEYTYPE_RSA,
        );

        // Create the private and public key
        $res = openssl_pkey_new($config);

        // Extract the private key from $res to $privKey
        openssl_pkey_export($res, $privKey);

        // Extract the public key from $res to $pubKey
        $pubKey = openssl_pkey_get_details($res);
        $pubKey = $pubKey['key'];

        $this->dkim_private = $privKey;
        $this->dkim_public = $pubKey;
    }

    /**
     * Add customer action log.
     */
    public function log($name, $customer, $add_datas = [])
    {
        $data = [
            'id' => $this->id,
            'name' => $this->name,
        ];

        $data = array_merge($data, $add_datas);

        Log::create([
            'customer_id' => $customer->id,
            'type' => 'sending_domain',
            'name' => $name,
            'data' => json_encode($data),
        ]);
    }

    public function startVerifying()
    {
        if ($this->sendingServer) {
            $tokens = $this->sendingServer->mapType()->verifyDomain($this->name);
        } else {
            // Below is a 'STANDARD' schema
            $tokens = $this->generateVerificationTokens();
        }

        $this->updateVerificationTokens($tokens);
    }

    public function generateVerificationTokens()
    {
        $tokens = [
            'identity' => [
                'type' => 'TXT',
                'name' => $this->getFullVerificationHostName(),
                'value' => $this->generateIdentityToken(),
            ],

            'dkim' => [
                [
                    'type' => 'TXT',
                    'name' => $this->getDomain()->getFullDkimHostName(),
                    'value' => doublequote($this->getDomain()->getDkimDnsRecord()),
                ]
            ],

            'results' => [
                'identity' => false,
                'dkim' => false,
            ]
        ];

        $spfRecord = $this->getSpf();

        if ($spfRecord) {
            $tokens['spf'] = [
                [
                    'type' => 'TXT',
                    'name' => $this->getDnsHostName(),
                    'value' => $this->getSpf(),
                ]
            ];

            $tokens['results']['spf'] = false;
        }

        return $tokens;
    }

    /**
     * Verify domain DNS.
     */
    public function verify()
    {
        $tokens = $this->getVerificationTokens();


        if ($this->sendingServer) {
            // Case: associated with a sending server
            // Verify Host, DKIM and SPF
            $server = $this->sendingServer->mapType();
            list($identity, $dkim, $spf, $finalStatus) = $server->checkDomainVerificationStatus($this);

            $tokens['results']['identity'] = $identity;
            $tokens['results']['dkim'] = $dkim;
            if (!is_null($spf)) {
                $tokens['results']['spf'] = $spf;
            }
            $this->status = $finalStatus ? self::STATUS_ACTIVE : self::STATUS_INACTIVE;
        } else {
            // Case: standalone
            $tokens['results']['identity'] = $this->verifyIdentity();
            $tokens['results']['dkim'] = $this->getDomain()->verifyDkim();

            if (array_key_exists('spf', $tokens)) {
                $host = Setting::get('spf_host');

                if (empty($host)) {
                    throw new Exception('There is no value for `spf_host`');
                }
                $tokens['results']['spf'] = $this->verifySpf($host);
            }

            $finalStatus = $tokens['results']['dkim'] && $tokens['results']['identity']; // SPF is optional
            $this->status = $finalStatus ? self::STATUS_ACTIVE : self::STATUS_INACTIVE;
        }

        $this->updateVerificationTokens($tokens);
    }

    /**
     * Verify TXT record, update domain status accordingly.
     *
     * @return mixed
     */
    public function verifyIdentity()
    {
        $tokens = $this->getVerificationTokens();
        $identityToken = $tokens['identity']['value'];
        $identityHostname = $tokens['identity']['name'];

        $results = collect(dns_get_record($identityHostname, DNS_TXT));
        $results = $results->where('type', 'TXT')
                           ->whereIn('txt', [$identityToken, doublequote($identityToken)]);

        return $results->isEmpty() ? false : true;
    }

    /**
     * Verify DKIM record, update domain status accordingly.
     *
     * @return mixed
     */
    public function verifySpf(string $ipOrHostname)
    {
        $result = spfcheck($ipOrHostname, $this->name);
        return $result == SPFCheck::RESULT_PASS;
    }

    /**
     * Set DKIM selector.
     *
     * @return string
     */
    public function setDkimSelector($dkim_selector)
    {
        if (preg_match('/^[a-z0-9]{1,24}$/', $dkim_selector)) {
            $this->dkim_selector = $dkim_selector;
            $this->save();

            return true;
        }

        return false;
    }

    // The second parameter is required
    // There are two types of domain verification
    // + Application verified
    // + Sending server verified (second parameter)
    public function createFromArray($attributes, $server)
    {
        // Default values for a NEW domain
        $this->status = self::STATUS_INACTIVE;

        // Get values from array
        $this->fill($attributes);

        // Validation
        $validator = Validator::make($this->getAttributes(), self::rules());

        // Additional unique validation
        $validator->after(function ($validator) {
            if (empty($this->name)) {
                return; // already catched by validation rules (name must be present!)
            }

            // Okay, if name is valid, then check if it is unique
            $notUniq = $this->customer->local()->sendingDomains()->where('name', $this->name)->exists();
            if ($notUniq) {
                $validator->errors()->add('name', "A sending domain with name `{$this->name}` already exists");
            }
        });

        // IMPORTANT: do not call fails() again in the controller, use is_null() with the second parameter
        if ($validator->fails()) {
            return [ $validator, null ];
        }

        DB::transaction(function () use ($server) {
            // Generate dkim keys
            // Used for signing emails
            $this->generateDkimKeys();

            // Save the domain record itself
            $this->save();

            if (!is_null($server)) {
                $this->sendingServer()->associate($server);
            }

            // Verify against server if any
            $this->startVerifying();
        });

        return [ $validator, $this ];
    }

    public function isAllowedBy($server)
    {
        if (!$server->allowOtherSendingDomains() && $this->sending_server_id != $server->id) {
            return false;
        } else {
            return true;
        }
    }

    public function isIdentityVerified()
    {
        return $this->getVerificationTokens()['results']['identity'] == true;
    }

    public function isDkimVerified()
    {
        return $this->getVerificationTokens()['results']['dkim'] == true;
    }

    public function isSpfNeeded()
    {
        $tokens = $this->getVerificationTokens();
        $spf = $tokens['spf'] ?? null;

        // 'spf' key must be present and not empty (null nor empty '')
        return !empty($spf);
    }

    public function isSpfVerified()
    {
        $tokens = $this->getVerificationTokens();
        if (!array_key_exists('spf', $tokens['results'])) {
            throw new Exception('SPF record does not exist');
        }
        return $tokens['results']['spf'] == true;
    }

    public function getVerificationTokens()
    {
        return json_decode($this->verification_token, true);
    }

    public function updateVerificationTokens($tokens)
    {
        $this->verification_token = json_encode($tokens);
        $this->save();
    }
}