- TenantMailConfigController: 목록, 편집, 저장, SMTP 테스트 API - TenantMailConfig, MailLog 모델 추가 - SmtpConnectionTester: SMTP 연결 테스트 서비스 (에러 코드, 트러블슈팅) - TenantMailService: 테넌트 설정 기반 메일 발송 (쿼터, Fallback) - config/mail-presets.php: Gmail/Naver/MS365 등 8개 SMTP 프리셋 - Blade 뷰: 테넌트 목록 현황 + 설정 폼 (프리셋 자동 채움, 연결 테스트) - 라우트 추가: /system/tenant-mail/*
246 lines
9.1 KiB
PHP
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 => '설정을 다시 확인하고 재시도하세요.',
|
|
};
|
|
}
|
|
}
|