Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
261 lines
7.3 KiB
PHP
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;
|
|
}
|
|
}
|