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/SendingServerSendGrid.php
<?php

/**
 * SendingServerSendGrid class.
 *
 * Abstract class for SendGrid sending servers
 *
 * 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 Acelle\Library\Log as MailLog;
use Acelle\Library\StringHelper;
use SendGrid\Mail\Mail;
use SendGrid\Mail\Content;
use SendGrid\Mail\Attachment;
use Acelle\Library\SendingServer\DomainVerificationInterface;

class SendingServerSendGrid extends SendingServer implements DomainVerificationInterface
{
    public const WEBHOOK = 'sendgrid';

    protected $table = 'sending_servers';
    public $client = null;
    public $isWebhookSetup = false;

    /**
     * Get authenticated to Mailgun and return the session object.
     *
     * @return mixed
     */
    public function client()
    {
        if (!$this->client) {
            MailLog::info('Using master account');
            $this->client = new \SendGrid($this->api_key);
        }

        return $this->client;
    }

    /**
     * Setup webhooks for processing bounce and feedback loop.
     *
     * @return mixed
     */
    public function setupWebhooks()
    {
        if ($this->isWebhookSetup) {
            return true;
        }

        MailLog::info('Setting up SendGrid webhooks');
        $subscribeUrl = StringHelper::joinUrl(Setting::get('url_delivery_handler'), self::WEBHOOK);
        $request_body = json_decode(
            '{
            "bounce": true,
            "click": false,
            "deferred": false,
            "delivered": false,
            "dropped": true,
            "enabled": true,
            "group_resubscribe": false,
            "group_unsubscribe": false,
            "open": false,
            "processed": false,
            "spam_report": true,
            "unsubscribe": false,
            "url": "'.$subscribeUrl.'"
            }'
        );
        $response = $this->client()->client->user()->webhooks()->event()->settings()->patch($request_body);

        if ($response->statusCode() == '200') {
            MailLog::info('Webhooks successfully set!');
        } else {
            throw new \Exception(sprintf('Cannot setup SendGrid webhook. Status code: %s. Body: %s', $response->statusCode(), $response->body()));
        }

        $this->isWebhookSetup = true;
    }

    /**
     * Get Message Id
     * Extract the message id from SendGrid response.
     *
     * @return string
     */
    public function getMessageId($headers)
    {
        preg_match('/(?<=X-Message-Id: ).*/', $headers, $matches);
        if (isset($matches[0])) {
            return $matches[0];
        } else {
            return;
        }
    }

    /**
     * Prepare the email object for sending.
     *
     * @return mixed
     */
    public function prepareEmail($message)
    {
        $fromEmail = array_keys($message->getFrom())[0];
        $fromName = (is_null($message->getFrom())) ? null : array_values($message->getFrom())[0];
        $toEmail = array_keys($message->getTo())[0];
        $toName = (is_null($message->getTo())) ? null : array_values($message->getTo())[0];
        $replyToEmail = (is_null($message->getReplyTo())) ? $fromEmail : array_keys($message->getReplyTo())[0];

        // Following RFC 1341, section 7.2
        //     If either text/html or text/plain are to be sent in your email
        //     text/plain needs to be first, followed by text/html, followed by any other content
        // So, use array_shift instead of array_pop
        // Also, sort the parts so that text/plain comes before text/html

        $parts = $message->getChildren();
        usort($parts, function ($a, $b) {
            if ($a->getContentType() == 'text/plain') {
                return -1;
            } elseif ($a->getContentType() == 'text/html') {
                return 0;
            } else {
                return 1;
            }
        });

        // skip attachment part
        $parts = array_map(function ($part) {
            if (method_exists($part, 'getDisposition')) { // only a part of type Swift_Mime_Attachment has this method
                // add later on
                return null;
            } else {
                return new Content($part->getContentType(), $part->getBody());
            }
        }, $parts);

        // remove null element
        $parts = array_filter($parts);

        $mail = new Mail();
        $mail->setFrom($fromEmail, $fromName);
        $mail->setSubject($message->getSubject());
        $mail->addTo($toEmail, $toName);

        // set Reply-To header
        $mail->setReplyTo($replyToEmail);

        foreach ($parts as $part) {
            $mail->addContent($part);
        }

        foreach ($message->getChildren() as $part) {
            if (method_exists($part, 'getDisposition')) {
                $filename = basename($part->getFilename());
                $encoded = base64_encode($part->getBody());
                $attachment = new Attachment();
                $attachment->setType($part->getContentType());
                $attachment->setContent($encoded);
                $attachment->setDisposition("attachment");
                $attachment->setFilename($filename);
                $mail->addAttachment($attachment);
            }
        }

        $preserved = [
            'Content-Transfer-Encoding',
            'Content-Type',
            'MIME-Version',
            'Date',
            'Message-ID',
            'From',
            'Subject',
            'To',
            'Reply-To',
            'Subject',
            'From',
        ];

        foreach ($message->getHeaders()->getAll() as $header) {
            if (!in_array($header->getFieldName(), $preserved)) {
                $mail->addHeader($header->getFieldName(), $header->getFieldBody());
            }
        }

        // to track bounce/feedback notification
        $mail->addCustomArg('runtime_message_id', $message->getHeaders()->get('X-Acelle-Message-Id')->getFieldBody());

        return $mail;
    }

    /**
     * Get verified identities (domains and email addresses).
     *
     * @return bool
     */
    public function syncIdentities()
    {
        $response = $this->client()->client->whitelabel()->domains()->get();
        $json = json_decode($response->body(), true);
        if (array_key_exists('errors', $json)) {
            throw new \Exception('Failed to connect to SendGrid: '.$response->body());
        }

        $identities = [];
        foreach ($json as $domain) {
            $name = $domain['domain'];
            $identities[$name] = ['VerificationStatus' => $domain['valid'] == true];
        }

        // list of identities that are added by customers/users
        $addedByUsers = $this->sendingDomains()->whereIn('name', array_keys($identities))->get();

        foreach ($addedByUsers as $domain) {
            if (array_key_exists($domain->name, $identities)) {
                $identities[$domain->name]['UserId'] = $domain->customer->id;
                $identities[$domain->name]['UserName'] = $domain->customer->displayName();
            }
        }

        $identityStore = $this->getIdentityStore();
        $identityStore->update($identities);
        $options = $this->getOptions();
        $options['identities'] = $identityStore->get();
        $this->setOptions($options);
        $this->save();
    }

    /**
     * Check the sending server settings, make sure it does work.
     *
     * @return bool
     */
    public function test()
    {
        $response = $this->client()->client->whitelabel()->domains()->get();
        $json = json_decode($response->body(), true);
        if (array_key_exists('errors', $json)) {
            throw new \Exception('Failed to connect to SendGrid: '.$response->body());
        }

        return true;
    }

    public function verifyDomain($domain): array
    {
        $body = json_decode('{
          "automatic_security": false,
          "custom_spf": false,
          "subdomain": "'.$this->getDefaultSubdomainName($domain).'",
          "default": false,
          "domain": "'.$domain.'"
        }');

        $response = $this->client()->client->whitelabel()->domains()->post($body);
        $result = json_decode($response->body(), true);

        if (array_key_exists('errors', $result)) {
            // in case of {"errors":[{"message":"An authenticated domain already exists for this URL. Please use a unique subdomain."}]}
            // => get the domain information

            $result = $this->getDomainVerificationInfo($domain);
            // if domain not found, then it is another error at first
            if (empty($result)) {
                throw new \Exception($response->body());
            }
        }

        $identityRecord = $result['dns']['mail_server'];
        $identityRecord['name'] = $identityRecord['host'];
        $identityRecord['value'] = $identityRecord['data'];
        $identityRecord['type'] = strtoupper($identityRecord['type']);

        $dkimRecord = $result['dns']['dkim'];
        $dkimRecord['name'] = $dkimRecord['host'];
        $dkimRecord['value'] = $dkimRecord['data'];
        $dkimRecord['type'] = strtoupper($dkimRecord['type']);

        $spfRecord = $result['dns']['subdomain_spf'];
        $spfRecord['name'] = $spfRecord['host'];
        $spfRecord['value'] = $spfRecord['data'];
        $spfRecord['type'] = strtoupper($spfRecord['type']);

        return [
            'identity' => $identityRecord,
            'dkim' => [ $dkimRecord ], // there may be more than one DKIM, AWS for example
            'spf' => [ $spfRecord ],
            'results' => [
                'identity' => false,
                'dkim' => false,
                'spf' => false,
            ],
        ];
    }

    public function checkDomainVerificationStatus($domain): array
    {
        $this->validateDomain($domain->name);
        $info = $this->getDomainVerificationInfo($domain->name);

        if (empty($info)) {
            throw new \Exception('Cannot verify domain against SendGrid');
        }

        $identity = $info['dns']['mail_server']['valid'];
        $dkim = $info['dns']['dkim']['valid'];
        $spf = $info['dns']['subdomain_spf']['valid'];

        $finalStatus = $identity && $dkim && $spf;

        return [ $identity, $dkim, $spf, $finalStatus ];
    }

    private function getDefaultSubdomainName($domain)
    {
        return substr(md5($domain), 0, 12);
    }

    private function getDomainVerificationInfo($domain)
    {
        $response = $this->client()->client->whitelabel()->domains()->get();
        $domains = json_decode($response->body(), true);

        $domains = array_values(array_filter($domains, function ($r) use ($domain) {
            return $domain == $r['domain'] && $this->getDefaultSubdomainName($domain) == $r['subdomain'];
        }));

        if (empty($domains)) {
            return null;
        } else {
            return $domains[0];
        }
    }

    public function validateDomain($domain)
    {
        // both dkim and identity
        $info = $this->getDomainVerificationInfo($domain);
        if (empty($info)) {
            return;
        }
        $result = $this->client()->client->whitelabel()->domains()->_($info['id'])->validate()->post();
        return $result;
    }

    /**
     * Allow user to verify his/her own sending domain against Acelle Mail.
     *
     * @return bool
     */
    public function allowVerifyingOwnDomains()
    {
        return false;
    }

    /**
     * Allow user to verify his/her own sending domain against Acelle Mail.
     *
     * @return bool
     */
    public function allowVerifyingOwnEmails()
    {
        return false;
    }

    /**
     * Allow user to verify his/her own emails against AWS.
     *
     * @return bool
     */
    public function allowVerifyingOwnDomainsRemotely()
    {
        return true;
    }

    /**
     * Allow user to verify his/her own emails against AWS.
     *
     * @return bool
     */
    public function allowVerifyingOwnEmailsRemotely()
    {
        return false;
    }

    public function setupBeforeSend($fromEmailAddress)
    {
        $this->setupWebhooks();
    }
}