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'); } } } }