Teren-app/app/Jobs/TestMailProfileConnection.php
2025-10-07 21:57:10 +02:00

150 lines
5.3 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\MailProfile;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class TestMailProfileConnection implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $timeout = 30;
public function __construct(public int $mailProfileId) {}
public function handle(): void
{
$profile = MailProfile::find($this->mailProfileId);
if (! $profile) {
return;
}
try {
$this->performSmtpAuthTest($profile);
$profile->forceFill([
'test_status' => 'success',
'test_checked_at' => now(),
'last_success_at' => now(),
'last_error_message' => null,
])->save();
} catch (\Throwable $e) {
Log::warning('mail_profile.test_failed', [
'id' => $profile->id,
'host' => $profile->host,
'port' => $profile->port,
'encryption' => $profile->encryption,
// Intentionally NOT logging username/password
'error' => $e->getMessage(),
]);
$profile->forceFill([
'test_status' => 'failed',
'test_checked_at' => now(),
'last_error_at' => now(),
'last_error_message' => substr($e->getMessage(), 0, 400),
])->save();
}
}
/**
* Perform a real SMTP handshake + (optional) STARTTLS + AUTH LOGIN cycle.
* Throws on any protocol / auth failure.
*/
protected function performSmtpAuthTest(MailProfile $profile): void
{
$host = $profile->host;
$port = (int) $profile->port;
$encryption = strtolower((string) ($profile->encryption ?? ''));
$username = $profile->username;
$password = trim($profile->decryptPassword() ?? '');
if (app()->environment('local')) {
Log::debug('mail_profile.test.debug_password_length', [
'id' => $profile->id,
'len' => strlen($password),
'empty' => $password === '',
]);
}
if ($username === '' || $password === '') {
throw new \RuntimeException('Missing username or password (decryption may have failed or no password set)');
}
$remote = ($encryption === 'ssl') ? 'ssl://'.$host : $host;
$errno = 0; $errstr = '';
$socket = @fsockopen($remote, $port, $errno, $errstr, 15);
if (! $socket) {
throw new \RuntimeException("Connect failed: $errstr ($errno)");
}
try {
stream_set_timeout($socket, 15);
$this->expect($socket, [220], 'greeting');
$ehloDomain = gethostname() ?: 'localhost';
$this->command($socket, "EHLO $ehloDomain\r\n", [250], 'EHLO');
if ($encryption === 'tls') {
$this->command($socket, "STARTTLS\r\n", [220], 'STARTTLS');
if (! stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
throw new \RuntimeException('STARTTLS negotiation failed');
}
// Re-issue EHLO after TLS per RFC
$this->command($socket, "EHLO $ehloDomain\r\n", [250], 'post-STARTTLS EHLO');
}
// AUTH LOGIN flow
$this->command($socket, "AUTH LOGIN\r\n", [334], 'AUTH LOGIN');
$this->command($socket, base64_encode($username)."\r\n", [334], 'AUTH username');
$authResp = $this->command($socket, base64_encode($password)."\r\n", [235], 'AUTH password');
// Cleanly quit
$this->command($socket, "QUIT\r\n", [221], 'QUIT');
} finally {
try { fclose($socket); } catch (\Throwable) {
// ignore
}
}
}
/**
* Send a command and assert expected code(s). Returns the last response line.
*/
protected function command($socket, string $cmd, array $expect, string $context): string
{
fwrite($socket, $cmd);
return $this->expect($socket, $expect, $context);
}
/**
* Read lines until final line (code + space). Validate code.
*/
protected function expect($socket, array $expectedCodes, string $context): string
{
$lines = [];
while (true) {
$line = fgets($socket, 2048);
if ($line === false) {
throw new \RuntimeException("SMTP read failure during $context");
}
$lines[] = rtrim($line, "\r\n");
if (preg_match('/^(\d{3})([ -])/', $line, $m)) {
$code = (int) $m[1];
$more = $m[2] === '-';
if (! $more) {
if (! in_array($code, $expectedCodes, true)) {
throw new \RuntimeException("Unexpected SMTP code $code during $context: ".implode(' | ', $lines));
}
return $line;
}
}
if (count($lines) > 50) {
throw new \RuntimeException('SMTP response too verbose');
}
}
}
}