argument('action'); switch ($action) { case 'list': $this->listBounces(); break; case 'stats': $this->showStats(); break; case 'suppress': $this->suppressEmail(); break; case 'unsuppress': $this->unsuppressEmail(); break; case 'cleanup': $this->cleanupOldBounces(); break; case 'check-thresholds': $this->checkThresholds(); break; default: $this->error("Invalid action. Use: list, stats, suppress, unsuppress, cleanup, check-thresholds"); return 1; } return 0; } /** * List bounced emails */ protected function listBounces() { $query = MailingBounce::query(); if ($type = $this->option('type')) { $query->where('bounce_type', $type); } $bounces = $query->orderBy('bounced_at', 'desc')->get(); if ($bounces->isEmpty()) { $this->info('No bounced emails found.'); return; } $headers = ['Email', 'Type', 'Reason', 'Bounced At', 'Suppressed']; $rows = $bounces->map(function ($bounce) { return [ $bounce->email, $bounce->bounce_type, Str::limit($bounce->bounce_reason, 50), $bounce->bounced_at->format('Y-m-d H:i'), $bounce->is_suppressed ? 'Yes' : 'No', ]; }); $this->table($headers, $rows); } /** * Show bounce statistics */ protected function showStats() { $config = timebank_config('mailing.bounce_thresholds', []); $windowDays = $config['counting_window_days'] ?? 30; $totalBounces = MailingBounce::count(); $suppressedEmails = MailingBounce::where('is_suppressed', true)->count(); $hardBounces = MailingBounce::where('bounce_type', 'hard')->count(); $softBounces = MailingBounce::where('bounce_type', 'soft')->count(); $recentBounces = MailingBounce::where('bounced_at', '>=', now()->subDays(7))->count(); $windowBounces = MailingBounce::where('bounced_at', '>=', now()->subDays($windowDays))->count(); $this->info("Mailing Bounce Statistics:"); $this->line("Total bounces: {$totalBounces}"); $this->line("Suppressed emails: {$suppressedEmails}"); $this->line("Hard bounces: {$hardBounces}"); $this->line("Soft bounces: {$softBounces}"); $this->line("Recent bounces (7 days): {$recentBounces}"); $this->line("Bounces in threshold window ({$windowDays} days): {$windowBounces}"); // Threshold configuration $this->line("\nThreshold Configuration:"); $this->line(" Suppression threshold: " . ($config['suppression_threshold'] ?? 3)); $this->line(" Verification reset threshold: " . ($config['verification_reset_threshold'] ?? 2)); $this->line(" Counting window: {$windowDays} days"); // Top bouncing domains $topDomains = MailingBounce::select(DB::raw('SUBSTRING_INDEX(email, "@", -1) as domain, COUNT(*) as count')) ->groupBy('domain') ->orderBy('count', 'desc') ->limit(5) ->get(); if ($topDomains->isNotEmpty()) { $this->line("\nTop bouncing domains:"); foreach ($topDomains as $domain) { $this->line(" {$domain->domain}: {$domain->count} bounces"); } } // Emails approaching thresholds $this->showEmailsApproachingThresholds($config); } /** * Show emails that are approaching bounce thresholds */ protected function showEmailsApproachingThresholds(array $config): void { $suppressionThreshold = $config['suppression_threshold'] ?? 3; $verificationResetThreshold = $config['verification_reset_threshold'] ?? 2; $windowDays = $config['counting_window_days'] ?? 30; // Find emails with high bounce counts but not yet suppressed $emailsNearThreshold = MailingBounce::select('email', DB::raw('COUNT(*) as bounce_count')) ->where('bounce_type', 'hard') ->where('bounced_at', '>=', now()->subDays($windowDays)) ->where('is_suppressed', false) ->groupBy('email') ->having('bounce_count', '>=', max(1, $verificationResetThreshold - 1)) ->orderBy('bounce_count', 'desc') ->limit(10) ->get(); if ($emailsNearThreshold->isNotEmpty()) { $this->line("\nEmails Approaching Thresholds:"); $headers = ['Email', 'Hard Bounces', 'Status']; $rows = $emailsNearThreshold->map(function ($item) use ($suppressionThreshold, $verificationResetThreshold) { $status = []; if ($item->bounce_count >= $suppressionThreshold) { $status[] = 'Will suppress'; } elseif ($item->bounce_count >= $verificationResetThreshold) { $status[] = 'Will reset verification'; } if (empty($status)) { $status[] = 'Approaching threshold'; } return [ $item->email, $item->bounce_count, implode(', ', $status) ]; }); $this->table($headers, $rows); } } /** * Check thresholds for a specific email or all emails */ protected function checkThresholds(): void { $email = $this->option('email'); if ($email) { $stats = MailingBounce::getBounceStats($email); $this->displayEmailStats($stats); } else { $this->info("Checking all emails against current thresholds..."); // Get all emails with bounces $emails = MailingBounce::distinct('email')->pluck('email'); $problematicEmails = []; foreach ($emails as $emailAddress) { $stats = MailingBounce::getBounceStats($emailAddress); $config = timebank_config('mailing.bounce_thresholds', []); $suppressionThreshold = $config['suppression_threshold'] ?? 3; $verificationResetThreshold = $config['verification_reset_threshold'] ?? 2; if ($stats['recent_hard_bounces'] >= $verificationResetThreshold) { $problematicEmails[] = $stats; } } if (empty($problematicEmails)) { $this->info("No emails exceed the current thresholds."); return; } $this->info("Found " . count($problematicEmails) . " emails exceeding thresholds:"); foreach ($problematicEmails as $stats) { $this->displayEmailStats($stats); $this->line('---'); } } } /** * Display bounce statistics for a specific email */ protected function displayEmailStats(array $stats): void { $config = timebank_config('mailing.bounce_thresholds', []); $suppressionThreshold = $config['suppression_threshold'] ?? 3; $verificationResetThreshold = $config['verification_reset_threshold'] ?? 2; $this->line("Email: {$stats['email']}"); $this->line(" Total bounces: {$stats['total_bounces']}"); $this->line(" Recent bounces ({$stats['window_days']} days): {$stats['recent_bounces']}"); $this->line(" Recent hard bounces: {$stats['recent_hard_bounces']}"); $this->line(" Currently suppressed: " . ($stats['is_suppressed'] ? 'Yes' : 'No')); // Status assessment if ($stats['recent_hard_bounces'] >= $suppressionThreshold) { $this->line(" 🔴 Status: Should be suppressed ({$stats['recent_hard_bounces']} >= {$suppressionThreshold})"); } elseif ($stats['recent_hard_bounces'] >= $verificationResetThreshold) { $this->line(" 🟡 Status: Should reset verification ({$stats['recent_hard_bounces']} >= {$verificationResetThreshold})"); } else { $this->line(" 🟢 Status: Below thresholds"); } } /** * Suppress a specific email */ protected function suppressEmail() { $email = $this->option('email'); if (!$email) { $email = $this->ask('Enter email address to suppress'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $this->error('Invalid email address.'); return; } MailingBounce::suppressEmail($email, 'Manually suppressed via command'); $this->info("Email {$email} has been suppressed."); } /** * Unsuppress a specific email */ protected function unsuppressEmail() { $email = $this->option('email'); if (!$email) { $email = $this->ask('Enter email address to unsuppress'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $this->error('Invalid email address.'); return; } $updated = MailingBounce::where('email', $email) ->update(['is_suppressed' => false]); if ($updated > 0) { $this->info("Email {$email} has been unsuppressed."); } else { $this->warn("Email {$email} was not found in bounce list."); } } /** * Clean up old bounces */ protected function cleanupOldBounces() { $days = $this->option('days') ?: 90; if (!$this->confirm("Delete bounces older than {$days} days? This will only remove old soft bounces, keeping hard bounces and suppressions.")) { return; } $deleted = MailingBounce::where('bounce_type', 'soft') ->where('is_suppressed', false) ->where('bounced_at', '<', now()->subDays($days)) ->delete(); $this->info("Deleted {$deleted} old soft bounce records."); } }