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