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/ai.naniguide.com/app/SendingServers/Identities/IdentityCreationService.php
<?php

namespace App\SendingServers\Identities;

use App\Model\Customer;
use App\Model\SendingIdentity;
use App\Model\SendingServer;
use App\Model\Setting;
use App\SendingServers\Capabilities\SupportsIdentitySync;
use App\SendingServers\Capabilities\SupportsRemoteDomainVerify;
use App\SendingServers\DomainVerification\CheckStatus;
use App\SendingServers\DomainVerification\DnsRecord;
use App\SendingServers\DomainVerification\RecordPurpose;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
use LogicException;

/**
 * Single creation gateway for sending identities.
 *
 * Determines `management_mode` automatically from the (server, kind)
 * pair — never user-chosen — and runs the appropriate side-effect:
 *   LOCAL_DKIM   : generate DKIM keypair + local DNS records (server-less only)
 *   VENDOR_SYNC  : push to vendor via SupportsRemoteDomainVerify::registerDomain (kind=domain)
 *                  or simply persist optimistic row (kind=email; vendor sync confirms later)
 *   MANUAL       : just insert; vendor (relay/Postal) handles signing if any
 */
final class IdentityCreationService
{
    public function create(
        ?SendingServer $server,
        IdentityKind $kind,
        string $value,
        Customer $customer,
        ?string $displayName = null,
    ): SendingIdentity {
        $mode = $this->determineMode($server);

        return match ($mode) {
            ManagementMode::LOCAL_DKIM  => $this->createLocalDkim($kind, $value, $customer, $displayName),
            ManagementMode::VENDOR_SYNC => $this->createVendorSync($server, $kind, $value, $customer, $displayName),
            ManagementMode::MANUAL      => $this->createManual($server, $kind, $value, $customer, $displayName),
        };
    }

    /**
     * Admin asserts that an identity exists on the server. Mode auto-detected
     * from the driver's sync capability — VENDOR_SYNC for SES/SendGrid/etc.
     * (admin-asserted; next IdentitySyncService run reconciles against vendor),
     * MANUAL for SMTP/Sendmail/Postal/custom relays (no auto-reconciliation).
     */
    public function createByAdmin(
        SendingServer $server,
        IdentityKind $kind,
        string $value,
        IdentityStatus $initialStatus = IdentityStatus::PENDING,
        ?string $displayName = null,
        bool $shared = false,
    ): SendingIdentity {
        $mode = $server->driver() instanceof SupportsIdentitySync
            ? ManagementMode::VENDOR_SYNC
            : ManagementMode::MANUAL;

        return SendingIdentity::create([
            'sending_server_id' => $server->id,
            'customer_id'       => null,
            'kind'              => $kind,
            'value'             => $value,
            'display_name'      => $displayName,
            'status'            => $initialStatus,
            'management_mode'   => $mode,
            'shared'            => $shared,
            'enabled'           => true,
            'last_synced_at'    => $mode === ManagementMode::VENDOR_SYNC ? now() : null,
        ]);
    }

    public function determineMode(?SendingServer $server): ManagementMode
    {
        if ($server === null) {
            return ManagementMode::LOCAL_DKIM;
        }
        if ($server->driver() instanceof SupportsIdentitySync) {
            return ManagementMode::VENDOR_SYNC;
        }
        return ManagementMode::MANUAL;
    }

    // ------------------------------------------------------------------

    private function createLocalDkim(
        IdentityKind $kind,
        string $value,
        Customer $customer,
        ?string $displayName,
    ): SendingIdentity {
        if ($kind !== IdentityKind::DOMAIN) {
            throw new InvalidArgumentException('LOCAL_DKIM only supports kind=domain');
        }

        $keys = $this->generateDkimKeys();
        $selector = Setting::get('dkim_selector') ?: 'acelle';
        $records = $this->buildLocalDnsRecords($value, $selector, $keys['public']);

        return DB::transaction(function () use ($value, $displayName, $customer, $keys, $selector, $records) {
            return SendingIdentity::create([
                'sending_server_id'    => null,
                'customer_id'          => $customer->id,
                'kind'                 => IdentityKind::DOMAIN,
                'value'                => $value,
                'display_name'         => $displayName,
                'status'               => IdentityStatus::PENDING,
                'management_mode'      => ManagementMode::LOCAL_DKIM,
                'shared'               => false,
                'enabled'              => true,
                'dkim_private'         => $keys['private'],
                'dkim_public'          => $keys['public'],
                'dkim_selector'        => $selector,
                'signing_enabled'      => true,
                'verification_records' => array_map(fn (DnsRecord $r) => $r->toArray(), $records),
            ]);
        });
    }

    private function createVendorSync(
        SendingServer $server,
        IdentityKind $kind,
        string $value,
        Customer $customer,
        ?string $displayName,
    ): SendingIdentity {
        $row = SendingIdentity::create([
            'sending_server_id' => $server->id,
            'customer_id'       => $customer->id,
            'kind'              => $kind,
            'value'             => $value,
            'display_name'      => $displayName,
            'status'            => IdentityStatus::PENDING,
            'management_mode'   => ManagementMode::VENDOR_SYNC,
            'shared'            => false,
            'enabled'           => true,
        ]);

        // Push domain to vendor if driver supports remote-verify; vendor
        // returns DNS records customer must add. Email-kind identities
        // typically use vendor's verify-by-click flow (SES SendCustomVerificationEmail
        // via SendsCustomVerificationEmail capability — separate concern).
        if ($kind === IdentityKind::DOMAIN
            && $server->driver() instanceof SupportsRemoteDomainVerify) {
            $result = $server->driver()->registerDomain($value);
            $row->applyVerificationResult($result);
        }

        return $row;
    }

    private function createManual(
        SendingServer $server,
        IdentityKind $kind,
        string $value,
        Customer $customer,
        ?string $displayName,
    ): SendingIdentity {
        return SendingIdentity::create([
            'sending_server_id' => $server->id,
            'customer_id'       => $customer->id,
            'kind'              => $kind,
            'value'             => $value,
            'display_name'      => $displayName,
            'status'            => IdentityStatus::PENDING,
            'management_mode'   => ManagementMode::MANUAL,
            'shared'            => false,
            'enabled'           => true,
        ]);
    }

    // ------------------------------------------------------------------
    // Local DKIM helpers (LOCAL_DKIM only)
    // ------------------------------------------------------------------

    /**
     * @return array{private: string, public: string}
     */
    private function generateDkimKeys(): array
    {
        $config = [
            'digest_alg'       => 'sha256',
            'private_key_bits' => 1024,
            'private_key_type' => OPENSSL_KEYTYPE_RSA,
        ];

        $res = openssl_pkey_new($config);
        if ($res === false) {
            throw new LogicException('openssl_pkey_new failed; check OpenSSL extension config');
        }

        openssl_pkey_export($res, $privKey);
        $details = openssl_pkey_get_details($res);

        return [
            'private' => $privKey,
            'public'  => $details['key'],
        ];
    }

    /**
     * @return list<DnsRecord>
     */
    private function buildLocalDnsRecords(string $domain, string $selector, string $publicKey): array
    {
        $records = [];

        // Ownership TXT — verification token (preserves legacy semantics)
        $records[] = new DnsRecord(
            purpose: RecordPurpose::OWNERSHIP,
            type: 'TXT',
            name: $this->localOwnershipHost($domain),
            value: $this->generateIdentityToken($domain),
            status: CheckStatus::PENDING,
        );

        // DKIM TXT — public key record
        $records[] = new DnsRecord(
            purpose: RecordPurpose::DKIM,
            type: 'TXT',
            name: "{$selector}._domainkey.{$domain}",
            value: $this->buildDkimDnsValue($publicKey),
            status: CheckStatus::PENDING,
            selector: $selector,
        );

        // SPF (optional) — only if Setting has spf_record configured
        $spf = Setting::get('spf_record');
        if (!empty($spf)) {
            $records[] = new DnsRecord(
                purpose: RecordPurpose::SPF,
                type: 'TXT',
                name: $domain,
                value: $spf,
                status: CheckStatus::PENDING,
            );
        }

        return $records;
    }

    private function localOwnershipHost(string $domain): string
    {
        $verifyHost = Setting::get('verification_hostname') ?: '_acelle';
        return "{$verifyHost}.{$domain}";
    }

    private function generateIdentityToken(string $domain): string
    {
        return base64_encode(md5(trim('SALT!' . $domain)));
    }

    private function buildDkimDnsValue(string $publicKey): string
    {
        $clean = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----'], '', $publicKey);
        $clean = trim(preg_replace('/\s+/', '', $clean));
        return sprintf('v=DKIM1; k=rsa; p=%s;', $clean);
    }
}