- TenantMailConfigController: 목록, 편집, 저장, SMTP 테스트 API - TenantMailConfig, MailLog 모델 추가 - SmtpConnectionTester: SMTP 연결 테스트 서비스 (에러 코드, 트러블슈팅) - TenantMailService: 테넌트 설정 기반 메일 발송 (쿼터, Fallback) - config/mail-presets.php: Gmail/Naver/MS365 등 8개 SMTP 프리셋 - Blade 뷰: 테넌트 목록 현황 + 설정 폼 (프리셋 자동 채움, 연결 테스트) - 라우트 추가: /system/tenant-mail/*
223 lines
6.7 KiB
PHP
223 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Mail;
|
|
|
|
use App\Models\Tenants\MailLog;
|
|
use App\Models\Tenants\TenantMailConfig;
|
|
use Illuminate\Mail\Mailable;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Mail;
|
|
|
|
class TenantMailService
|
|
{
|
|
/**
|
|
* 테넌트 설정을 적용하여 메일 발송
|
|
*/
|
|
public function send(
|
|
Mailable $mailable,
|
|
string|array $to,
|
|
?int $tenantId = null,
|
|
bool $sync = false
|
|
): MailLog {
|
|
$tenantId = $tenantId ?? session('selected_tenant_id', 1);
|
|
$config = $this->getConfig($tenantId);
|
|
|
|
// 메일 로그 생성 (queued 상태)
|
|
$mailLog = MailLog::create([
|
|
'tenant_id' => $tenantId,
|
|
'mailable_type' => class_basename($mailable),
|
|
'to_address' => is_array($to) ? implode(', ', $to) : $to,
|
|
'from_address' => $config ? $config->from_address : config('mail.from.address'),
|
|
'subject' => $this->getSubject($mailable),
|
|
'status' => 'queued',
|
|
'options' => [
|
|
'provider_used' => $config?->provider ?? 'platform',
|
|
],
|
|
]);
|
|
|
|
// 쿼터 확인
|
|
if ($config && ! $this->checkQuota($tenantId, $config)) {
|
|
$mailLog->update([
|
|
'status' => 'failed',
|
|
'options' => array_merge($mailLog->options ?? [], [
|
|
'error_message' => '일일 발송 한도 초과',
|
|
]),
|
|
]);
|
|
Log::warning("Mail quota exceeded for tenant {$tenantId}");
|
|
|
|
return $mailLog;
|
|
}
|
|
|
|
try {
|
|
$this->configureMail($config);
|
|
$this->applyBranding($mailable, $config);
|
|
|
|
if ($sync) {
|
|
Mail::to($to)->send($mailable);
|
|
} else {
|
|
Mail::to($to)->queue($mailable);
|
|
}
|
|
|
|
$mailLog->update([
|
|
'status' => 'sent',
|
|
'sent_at' => now(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
Log::error("Mail send failed for tenant {$tenantId}: ".$e->getMessage());
|
|
|
|
// Fallback: 플랫폼 기본 SMTP
|
|
if ($config && $config->provider !== 'platform') {
|
|
try {
|
|
$this->resetMailConfig();
|
|
Mail::to($to)->send($mailable);
|
|
|
|
$mailLog->update([
|
|
'status' => 'sent',
|
|
'sent_at' => now(),
|
|
'options' => array_merge($mailLog->options ?? [], [
|
|
'fallback_used' => true,
|
|
'original_error' => $e->getMessage(),
|
|
]),
|
|
]);
|
|
|
|
return $mailLog;
|
|
} catch (\Exception $fallbackException) {
|
|
// Fallback도 실패
|
|
}
|
|
}
|
|
|
|
$mailLog->update([
|
|
'status' => 'failed',
|
|
'options' => array_merge($mailLog->options ?? [], [
|
|
'error_message' => $e->getMessage(),
|
|
'retry_count' => ($mailLog->getOption('retry_count', 0)) + 1,
|
|
]),
|
|
]);
|
|
}
|
|
|
|
return $mailLog;
|
|
}
|
|
|
|
/**
|
|
* 테넌트 메일 설정 조회
|
|
*/
|
|
private function getConfig(int $tenantId): ?TenantMailConfig
|
|
{
|
|
return TenantMailConfig::withoutGlobalScopes()
|
|
->where('tenant_id', $tenantId)
|
|
->where('is_active', true)
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* 일일 발송 쿼터 확인
|
|
*/
|
|
private function checkQuota(int $tenantId, TenantMailConfig $config): bool
|
|
{
|
|
$todayCount = MailLog::withoutGlobalScopes()
|
|
->where('tenant_id', $tenantId)
|
|
->whereDate('created_at', today())
|
|
->whereIn('status', ['queued', 'sent'])
|
|
->count();
|
|
|
|
return $todayCount < $config->daily_limit;
|
|
}
|
|
|
|
/**
|
|
* SMTP 설정 동적 교체
|
|
*/
|
|
private function configureMail(?TenantMailConfig $config): void
|
|
{
|
|
if (! $config || $config->provider === 'platform') {
|
|
return;
|
|
}
|
|
|
|
if ($config->provider === 'smtp') {
|
|
config([
|
|
'mail.mailers.smtp.host' => $config->getSmtpHost(),
|
|
'mail.mailers.smtp.port' => $config->getSmtpPort(),
|
|
'mail.mailers.smtp.encryption' => $config->getSmtpEncryption(),
|
|
'mail.mailers.smtp.username' => $config->getSmtpUsername(),
|
|
'mail.mailers.smtp.password' => $config->getSmtpPassword(),
|
|
'mail.from.address' => $config->from_address,
|
|
'mail.from.name' => $config->from_name,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기본 메일 설정 복원
|
|
*/
|
|
private function resetMailConfig(): void
|
|
{
|
|
config([
|
|
'mail.mailers.smtp.host' => env('MAIL_HOST'),
|
|
'mail.mailers.smtp.port' => env('MAIL_PORT'),
|
|
'mail.mailers.smtp.encryption' => env('MAIL_ENCRYPTION'),
|
|
'mail.mailers.smtp.username' => env('MAIL_USERNAME'),
|
|
'mail.mailers.smtp.password' => env('MAIL_PASSWORD'),
|
|
'mail.from.address' => env('MAIL_FROM_ADDRESS'),
|
|
'mail.from.name' => env('MAIL_FROM_NAME'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 브랜딩 데이터 적용
|
|
*/
|
|
private function applyBranding(Mailable $mailable, ?TenantMailConfig $config): void
|
|
{
|
|
if (! $config) {
|
|
return;
|
|
}
|
|
|
|
$mailable->from($config->from_address, $config->from_name);
|
|
|
|
if ($config->reply_to) {
|
|
$mailable->replyTo($config->reply_to);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mailable 제목 추출
|
|
*/
|
|
private function getSubject(Mailable $mailable): string
|
|
{
|
|
try {
|
|
$reflection = new \ReflectionProperty($mailable, 'subject');
|
|
$subject = $reflection->getValue($mailable);
|
|
if ($subject) {
|
|
return $subject;
|
|
}
|
|
} catch (\ReflectionException $e) {
|
|
// subject 속성이 없는 경우
|
|
}
|
|
|
|
return class_basename($mailable);
|
|
}
|
|
|
|
/**
|
|
* 오늘 발송 건수 조회
|
|
*/
|
|
public function getTodayCount(int $tenantId): int
|
|
{
|
|
return MailLog::withoutGlobalScopes()
|
|
->where('tenant_id', $tenantId)
|
|
->whereDate('created_at', today())
|
|
->whereIn('status', ['queued', 'sent'])
|
|
->count();
|
|
}
|
|
|
|
/**
|
|
* 이번 달 발송 건수 조회
|
|
*/
|
|
public function getMonthCount(int $tenantId): int
|
|
{
|
|
return MailLog::withoutGlobalScopes()
|
|
->where('tenant_id', $tenantId)
|
|
->whereMonth('created_at', now()->month)
|
|
->whereYear('created_at', now()->year)
|
|
->whereIn('status', ['queued', 'sent'])
|
|
->count();
|
|
}
|
|
}
|