Files
isaaccladandCursor b6c8ac343f Extract Ladill Servers as standalone app at servers.ladill.com.
VPS and dedicated server ordering, managed panels, SSO, billing, and launcher integration — forked from hosting infrastructure with server-focused routes and dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 19:18:30 +00:00

261 lines
7.3 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
class EmailDomain extends Model
{
use HasFactory;
public const STATUS_PENDING = 'pending';
public const STATUS_VERIFYING = 'verifying';
public const STATUS_ACTIVE = 'active';
public const STATUS_SUSPENDED = 'suspended';
public const METHOD_NAMESERVER = 'nameserver';
public const METHOD_DNS_RECORD = 'dns_record';
public const FREE_MAILBOXES_LIMIT = 5;
public const PAID_MAILBOX_PRICE_CEDIS = 5;
public const SUBSCRIPTION_MONTHLY_PRICE_CEDIS = 5;
protected $fillable = [
'user_id',
'email_server_id',
'domain',
'status',
'verification_method',
'verification_token',
'ns_expected',
'ns_observed',
'ns_checked_at',
'verified_at',
'mail_ready_at',
'free_mailboxes_limit',
'paid_mailboxes_count',
'dns_records',
'dns_verification_status',
'dns_last_checked_at',
'metadata',
];
protected $casts = [
'ns_expected' => '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;
}
}