'array', 'ns_observed' => 'array', 'ns_checked_at' => 'datetime', 'verified_at' => 'datetime', 'mail_ready_at' => 'datetime', 'dns_records' => 'array', 'dns_verification_status' => 'array', 'dns_last_checked_at' => 'datetime', 'metadata' => 'array', ]; public function user(): BelongsTo { return $this->belongsTo(User::class); } public function emailServer(): BelongsTo { return $this->belongsTo(EmailServer::class); } public function mailboxes(): HasMany { return $this->hasMany(Mailbox::class, 'email_domain_id'); } public function dkimKeys(): HasMany { return $this->hasMany(EmailDomainDkimKey::class); } public function activeDkimKey(): HasOne { return $this->hasOne(EmailDomainDkimKey::class)->where('status', 'deployed')->latest(); } public function isPending(): bool { return $this->status === self::STATUS_PENDING; } public function isVerifying(): bool { return $this->status === self::STATUS_VERIFYING; } public function isActive(): bool { return $this->status === self::STATUS_ACTIVE; } public function isSuspended(): bool { return $this->status === self::STATUS_SUSPENDED; } public function isReadyForMail(): bool { // Domain is ready for mail if it's active (verified) // mail_ready_at is set when infrastructure is provisioned, but we allow // mailbox creation as soon as domain is verified return $this->isActive(); } public function usesNameserverVerification(): bool { return $this->verification_method === self::METHOD_NAMESERVER; } public function usesDnsRecordVerification(): bool { return $this->verification_method === self::METHOD_DNS_RECORD; } public function getMailboxCountAttribute(): int { return $this->mailboxes()->count(); } public function getFreeMailboxesLimitAttribute($value): int { $base = $value ?? self::FREE_MAILBOXES_LIMIT; // Check if user has hosting accounts with higher email quotas $hostingAllowance = $this->getHostingEmailAllowance(); return max($base, $hostingAllowance); } /** * Get the email allowance from the user's hosting accounts for this domain. */ public function getHostingEmailAllowance(): int { $hostingAccount = HostingAccount::query() ->where('user_id', $this->user_id) ->where('status', HostingAccount::STATUS_ACTIVE) ->whereHas('sites', fn ($q) => $q->where('domain', $this->domain)) ->with('product') ->first(); if (! $hostingAccount) { // Also check primary_domain $hostingAccount = HostingAccount::query() ->where('user_id', $this->user_id) ->where('status', HostingAccount::STATUS_ACTIVE) ->where('primary_domain', $this->domain) ->with('product') ->first(); } if (! $hostingAccount) { return 0; } return $hostingAccount->freeEmailAllowance() ?? 0; } /** * Get per-record DNS verification results. */ public function getDnsRecordStatuses(): array { return $this->dns_verification_status ?? []; } /** * Whether an authentication record (spf|dkim|dmarc) should display as * verified. The per-record dns_verification_status map is only populated by * the DNS-record verification path; nameserver-managed / Ladill-owned domains * (we publish their DNS) leave it empty even when fully configured. So an * active domain necessarily has SPF + DKIM in place (required to activate), * and DMARC is published for nameserver-managed domains. */ public function authVerified(string $key): bool { if (($this->getDnsRecordStatuses()[$key]['status'] ?? null) === 'verified') { return true; } return match ($key) { 'spf', 'dkim' => $this->isActive(), 'dmarc' => $this->isActive() && $this->usesNameserverVerification(), default => false, }; } /** * Check if all required DNS records are verified. */ public function allDnsRecordsVerified(): bool { $statuses = $this->getDnsRecordStatuses(); if (empty($statuses)) { return false; } // All required records must be verified foreach ($statuses as $record) { if (($record['required'] ?? true) && ($record['status'] ?? 'pending') !== 'verified') { return false; } } return true; } /** * Count verified and total required DNS records. */ public function dnsVerificationProgress(): array { $statuses = $this->getDnsRecordStatuses(); $required = collect($statuses)->filter(fn ($r) => $r['required'] ?? true); return [ 'total' => $required->count(), 'verified' => $required->where('status', 'verified')->count(), 'pending' => $required->where('status', '!=', 'verified')->count(), ]; } public function getFreeMailboxesUsedAttribute(): int { return $this->mailboxes()->where('is_paid', false)->count(); } public function getPaidMailboxesUsedAttribute(): int { return $this->mailboxes()->where('is_paid', true)->count(); } public function getFreeMailboxesRemainingAttribute(): int { return max(0, $this->free_mailboxes_limit - $this->free_mailboxes_used); } public function canCreateFreeMailbox(): bool { return $this->free_mailboxes_remaining > 0; } public function requiresPaymentForNextMailbox(): bool { return !$this->canCreateFreeMailbox(); } public function monthlySubscriptionPrice(): float { return self::SUBSCRIPTION_MONTHLY_PRICE_CEDIS; } }