diff --git a/app/app/Services/External/SmtpLivenessProbe.php b/app/app/Services/External/SmtpLivenessProbe.php new file mode 100644 index 00000000..aebcd5c8 --- /dev/null +++ b/app/app/Services/External/SmtpLivenessProbe.php @@ -0,0 +1,65 @@ +connector = $connector; + } + + public function serviceKey(): string + { + return 'email'; + } + + public function check(): LivenessReading + { + try { + $banner = ($this->connector ?? $this->defaultConnector())(); + if (! str_starts_with(ltrim($banner), '220')) { + return LivenessReading::down('email', 'SMTP-баннер не 220: '.mb_substr(trim($banner), 0, 120)); + } + + return LivenessReading::alive('email', 'SMTP отвечает'); + } catch (\Throwable $e) { + return LivenessReading::down('email', $e->getMessage()); + } + } + + /** @return callable():string */ + private function defaultConnector(): callable + { + return function (): string { + $host = (string) config('services.smtp_probe.host'); + $port = (int) config('services.smtp_probe.port'); + $timeout = (int) config('services.smtp_probe.timeout', 5); + // 465 — implicit TLS; ssl:// нужен на connect. + $scheme = $port === 465 ? 'ssl://' : 'tcp://'; + $fp = @stream_socket_client($scheme.$host.':'.$port, $errno, $errstr, $timeout); + if ($fp === false) { + throw new \RuntimeException($errstr !== '' ? $errstr : 'Connection refused (errno '.$errno.')'); + } + try { + stream_set_timeout($fp, $timeout); + $line = fgets($fp, 512); + + return $line === false ? '' : $line; + } finally { + fclose($fp); + } + }; + } +} diff --git a/app/config/services.php b/app/config/services.php index dd79f9cc..d68401af 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -95,6 +95,14 @@ return [ 'amber_floor_rub' => (int) env('YC_AMBER_FLOOR_RUB', 5000), ], + // Healthcheck доступности SMTP (Yandex 360) для плитки внешних сервисов. + // Только connect+баннер, без логина/отправки. Дефолты — под Yandex 360. + 'smtp_probe' => [ + 'host' => env('SMTP_PROBE_HOST', env('MAIL_HOST', 'smtp.yandex.ru')), + 'port' => (int) env('SMTP_PROBE_PORT', 465), + 'timeout' => (int) env('SMTP_PROBE_TIMEOUT', 5), + ], + // G7-A: клиентская «Помощь». 'support' => [ 'email' => env('SUPPORT_EMAIL', 'support@liderra.ru'), diff --git a/app/tests/Unit/External/SmtpLivenessProbeTest.php b/app/tests/Unit/External/SmtpLivenessProbeTest.php new file mode 100644 index 00000000..537e6db7 --- /dev/null +++ b/app/tests/Unit/External/SmtpLivenessProbeTest.php @@ -0,0 +1,26 @@ + "220 smtp.yandex.ru ESMTP\r\n"); + $r = $probe->check(); + expect($r->serviceKey)->toBe('email'); + expect($r->light)->toBe('green'); +}); + +it('красный, когда соединитель бросил (порт недоступен)', function () { + $probe = new SmtpLivenessProbe(function () { + throw new RuntimeException('Connection refused'); + }); + $r = $probe->check(); + expect($r->light)->toBe('red'); + expect($r->detail)->toContain('refused'); +}); + +it('красный, когда баннер не 220', function () { + $probe = new SmtpLivenessProbe(fn () => "554 blocked\r\n"); + expect($probe->check()->light)->toBe('red'); +});