Files
sam-manage/app/Services/Mail/SmtpConnectionTester.php
김보곤 a0ba7fc13f feat: [email] 테넌트 이메일 설정 관리 기능 추가
- TenantMailConfigController: 목록, 편집, 저장, SMTP 테스트 API
- TenantMailConfig, MailLog 모델 추가
- SmtpConnectionTester: SMTP 연결 테스트 서비스 (에러 코드, 트러블슈팅)
- TenantMailService: 테넌트 설정 기반 메일 발송 (쿼터, Fallback)
- config/mail-presets.php: Gmail/Naver/MS365 등 8개 SMTP 프리셋
- Blade 뷰: 테넌트 목록 현황 + 설정 폼 (프리셋 자동 채움, 연결 테스트)
- 라우트 추가: /system/tenant-mail/*
2026-03-12 07:42:17 +09:00

246 lines
9.1 KiB
PHP

<?php
namespace App\Services\Mail;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
class SmtpConnectionTester
{
/**
* SMTP 연결 테스트
*
* @return array{success: bool, message: string, response_time_ms: int, server_banner: string|null, error_code: string|null}
*/
public function test(
string $host,
int $port,
string $encryption,
string $username,
string $password,
?string $testRecipient = null
): array {
$startTime = microtime(true);
try {
// Symfony Mailer를 사용한 SMTP 연결 테스트
$transport = new EsmtpTransport($host, $port, $encryption === 'ssl');
// TLS 설정
if ($encryption === 'tls') {
/** @var SocketStream $stream */
$stream = $transport->getStream();
$stream->disableTls();
}
$transport->setUsername($username);
$transport->setPassword($password);
// 연결 시도 (타임아웃 10초)
$transport->start();
$responseTimeMs = (int) ((microtime(true) - $startTime) * 1000);
// 테스트 메일 발송 (선택)
$testMailSent = false;
if ($testRecipient) {
$testMailSent = $this->sendTestMail($transport, $username, $testRecipient);
}
$transport->stop();
return [
'success' => true,
'message' => 'SMTP 연결 성공',
'response_time_ms' => $responseTimeMs,
'server_banner' => $host.':'.$port,
'error_code' => null,
'test_mail_sent' => $testMailSent,
];
} catch (\Symfony\Component\Mailer\Exception\TransportExceptionInterface $e) {
$responseTimeMs = (int) ((microtime(true) - $startTime) * 1000);
return $this->parseTransportError($e, $responseTimeMs);
} catch (\Exception $e) {
$responseTimeMs = (int) ((microtime(true) - $startTime) * 1000);
return [
'success' => false,
'message' => '연결 실패: '.$e->getMessage(),
'response_time_ms' => $responseTimeMs,
'server_banner' => null,
'error_code' => 'UNKNOWN',
];
}
}
/**
* Laravel Mail 파사드를 사용한 SMTP 연결 테스트 (대체 방식)
*/
public function testViaLaravel(
string $host,
int $port,
string $encryption,
string $username,
string $password,
?string $testRecipient = null
): array {
$startTime = microtime(true);
try {
$config = [
'transport' => 'smtp',
'host' => $host,
'port' => $port,
'encryption' => $encryption,
'username' => $username,
'password' => $password,
'timeout' => 10,
];
$transport = \Illuminate\Support\Facades\Mail::createSymfonyTransport($config);
$transport->start();
$responseTimeMs = (int) ((microtime(true) - $startTime) * 1000);
$testMailSent = false;
if ($testRecipient) {
$testMailSent = $this->sendTestMailViaLaravel($config, $testRecipient, $username);
}
$transport->stop();
return [
'success' => true,
'message' => 'SMTP 연결 성공',
'response_time_ms' => $responseTimeMs,
'server_banner' => $host.':'.$port,
'error_code' => null,
'test_mail_sent' => $testMailSent,
];
} catch (\Exception $e) {
$responseTimeMs = (int) ((microtime(true) - $startTime) * 1000);
return $this->parseError($e, $responseTimeMs);
}
}
private function sendTestMail($transport, string $from, string $to): bool
{
try {
$email = (new \Symfony\Component\Mime\Email)
->from($from)
->to($to)
->subject('[SAM] SMTP 연결 테스트')
->text('이 메일은 SAM 시스템의 SMTP 연결 테스트입니다. 정상적으로 수신되었다면 설정이 올바릅니다.');
$transport->send($email);
return true;
} catch (\Exception $e) {
return false;
}
}
private function sendTestMailViaLaravel(array $config, string $to, string $from): bool
{
try {
config(['mail.mailers.smtp_test' => $config]);
config(['mail.mailers.smtp_test.transport' => 'smtp']);
\Illuminate\Support\Facades\Mail::mailer('smtp_test')
->raw('이 메일은 SAM 시스템의 SMTP 연결 테스트입니다. 정상적으로 수신되었다면 설정이 올바릅니다.', function ($message) use ($to, $from) {
$message->to($to)
->from($from, 'SAM 시스템')
->subject('[SAM] SMTP 연결 테스트');
});
return true;
} catch (\Exception $e) {
return false;
}
}
private function parseTransportError(\Symfony\Component\Mailer\Exception\TransportExceptionInterface $e, int $responseTimeMs): array
{
$message = $e->getMessage();
// 에러 패턴 매칭
if (str_contains($message, 'Connection refused') || str_contains($message, 'Connection timed out')) {
return [
'success' => false,
'message' => 'SMTP 서버에 접속할 수 없습니다 — 호스트/포트를 확인하세요',
'response_time_ms' => $responseTimeMs,
'server_banner' => null,
'error_code' => 'CONN_REFUSED',
];
}
if (str_contains($message, 'SSL') || str_contains($message, 'TLS') || str_contains($message, 'handshake')) {
return [
'success' => false,
'message' => '암호화 연결 실패 — 암호화 방식(TLS/SSL)을 확인하세요',
'response_time_ms' => $responseTimeMs,
'server_banner' => null,
'error_code' => 'TLS_FAILED',
];
}
if (str_contains($message, 'Authentication') || str_contains($message, '535') || str_contains($message, 'credentials')) {
return [
'success' => false,
'message' => '인증 실패 — 사용자명/비밀번호(앱 비밀번호)를 확인하세요',
'response_time_ms' => $responseTimeMs,
'server_banner' => null,
'error_code' => 'AUTH_FAILED',
];
}
if (str_contains($message, 'timed out') || str_contains($message, 'Timeout')) {
return [
'success' => false,
'message' => '연결 시간 초과 — 잠시 후 다시 시도하세요',
'response_time_ms' => $responseTimeMs,
'server_banner' => null,
'error_code' => 'TIMEOUT',
];
}
return [
'success' => false,
'message' => '연결 실패: '.$message,
'response_time_ms' => $responseTimeMs,
'server_banner' => null,
'error_code' => 'UNKNOWN',
];
}
private function parseError(\Exception $e, int $responseTimeMs): array
{
return $this->parseTransportError(
new \Symfony\Component\Mailer\Exception\TransportException($e->getMessage(), 0, $e),
$responseTimeMs
);
}
/**
* 에러 코드별 트러블슈팅 메시지
*/
public static function getTroubleshoot(string $errorCode, ?string $preset = null): string
{
return match ($errorCode) {
'CONN_REFUSED' => '호스트 주소와 포트 번호가 정확한지 확인하세요. 방화벽이 해당 포트를 차단하고 있을 수 있습니다.',
'TLS_FAILED' => 'TLS와 SSL을 바꿔서 시도하세요. 포트 587은 보통 TLS, 포트 465는 SSL을 사용합니다.',
'AUTH_FAILED' => match ($preset) {
'gmail' => 'Gmail은 앱 비밀번호가 필요합니다. Google 계정 > 보안 > 2단계 인증 활성화 후 앱 비밀번호를 생성하세요.',
'naver' => '네이버 메일 설정에서 SMTP 사용을 활성화하고, 앱 비밀번호를 생성하세요.',
'daum' => '카카오 계정 > 보안 > 앱 비밀번호를 생성하세요.',
default => '사용자명과 비밀번호를 확인하세요. 일반 비밀번호가 아닌 앱 비밀번호가 필요할 수 있습니다.',
},
'SMTP_DISABLED' => '메일 서비스 설정에서 SMTP 사용을 활성화하세요.',
'TIMEOUT' => '네트워크 상태를 확인하고 잠시 후 다시 시도하세요.',
default => '설정을 다시 확인하고 재시도하세요.',
};
}
}