Mail support testing faze
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user