106 lines
3.5 KiB
PHP
106 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Models\Concerns\HasUuid;
|
|
use App\Services\Secrets\SecretCipher;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
/**
|
|
* One sending address.
|
|
*
|
|
* The password is stored under SECRETS_KEY rather than APP_KEY, and therefore
|
|
* not through Laravel's `encrypted` cast — that cast uses APP_KEY, which is
|
|
* rotated as ordinary maintenance and would take every mailbox with it.
|
|
*/
|
|
class Mailbox extends Model
|
|
{
|
|
use HasFactory, HasUuid;
|
|
|
|
protected $fillable = [
|
|
'key', 'address', 'display_name', 'username',
|
|
'password', 'no_reply', 'active', 'authenticates', 'last_verified_at',
|
|
];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'no_reply' => 'boolean',
|
|
'active' => 'boolean',
|
|
'authenticates' => 'boolean',
|
|
'last_verified_at' => 'datetime',
|
|
];
|
|
}
|
|
|
|
public static function findByKey(string $key): ?self
|
|
{
|
|
return static::query()->where('key', $key)->first();
|
|
}
|
|
|
|
/** Plaintext in, ciphertext to the column. */
|
|
public function setPasswordAttribute(?string $value): void
|
|
{
|
|
$this->attributes['password'] = ($value === null || $value === '')
|
|
? null
|
|
: app(SecretCipher::class)->encrypt($value);
|
|
}
|
|
|
|
/** Ciphertext from the column, plaintext out. */
|
|
public function getPasswordAttribute(?string $value): ?string
|
|
{
|
|
return ($value === null || $value === '')
|
|
? null
|
|
: app(SecretCipher::class)->decrypt($value);
|
|
}
|
|
|
|
/** The SMTP user: an explicit one, or the address itself. */
|
|
public function smtpUsername(): string
|
|
{
|
|
return $this->username !== null && $this->username !== ''
|
|
? $this->username
|
|
: $this->address;
|
|
}
|
|
|
|
/**
|
|
* last_verified_at proves a successful test against the config THIS
|
|
* mailbox held at the time — an address, username or authenticates
|
|
* change (or a new password) invalidates only this row. Sets the
|
|
* attribute without saving: EditMailbox::save() calls this alongside
|
|
* other attribute changes so the whole edit lands in one write, not two.
|
|
*/
|
|
public function invalidateVerification(): void
|
|
{
|
|
$this->last_verified_at = null;
|
|
}
|
|
|
|
/**
|
|
* The mirror of invalidateVerification() for the server card
|
|
* (Admin\Mail::saveServer()): host, port and encryption are shared by
|
|
* every mailbox at once, not a per-row concept, so a change there
|
|
* invalidates all of them in one write rather than each row deciding
|
|
* independently — Codex R15#6, P2. Scoped to rows that still have
|
|
* something to clear, so saving unrelated server settings does not bump
|
|
* every mailbox's updated_at for nothing.
|
|
*/
|
|
public static function invalidateAllVerifications(): int
|
|
{
|
|
return static::query()->whereNotNull('last_verified_at')->update(['last_verified_at' => null]);
|
|
}
|
|
|
|
/**
|
|
* Enough to send with: an address, and a password ONLY when this mailbox
|
|
* actually authenticates.
|
|
*
|
|
* A trusted local or private-network relay can legitimately need no
|
|
* password at all (Codex R15#4, P1b) — requiring one unconditionally
|
|
* would refuse a relay that was never broken, just unauthenticated.
|
|
*/
|
|
public function isConfigured(): bool
|
|
{
|
|
return $this->active
|
|
&& $this->address !== ''
|
|
&& (! $this->authenticates || $this->getRawOriginal('password') !== null);
|
|
}
|
|
}
|