Created
February 12, 2026 13:34
-
-
Save zach2825/381b82600ff016529ff480ada70236e8 to your computer and use it in GitHub Desktop.
email-spams.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Services; | |
| use App\Models\ContactInquiry; | |
| use App\Models\SpamPattern; | |
| class SpamScorer | |
| { | |
| public const THRESHOLD_SPAM = 70; | |
| public const THRESHOLD_SUSPECT = 40; | |
| private static array $disposableDomains = [ | |
| 'mailinator.com', 'guerrillamail.com', 'tempmail.com', 'throwaway.email', | |
| 'yopmail.com', 'sharklasers.com', 'guerrillamailblock.com', 'grr.la', | |
| 'dispostable.com', 'mailnesia.com', 'maildrop.cc', 'discard.email', | |
| 'temp-mail.org', 'fakeinbox.com', 'trashmail.com', 'getnada.com', | |
| 'mohmal.com', '10minutemail.com', 'minutemail.com', 'tempail.com', | |
| ]; | |
| private static array $spamPhrases = [ | |
| 'buy now', 'click here', 'free money', 'act now', 'limited time', | |
| 'congratulations', 'you have won', 'dear sir', 'dear madam', | |
| 'nigerian prince', 'wire transfer', 'bitcoin', 'cryptocurrency', | |
| 'make money fast', 'work from home', 'double your', 'guaranteed income', | |
| 'seo services', 'link building', 'web traffic', 'backlinks', | |
| 'casino', 'viagra', 'cialis', 'pharmacy', 'diet pills', | |
| ]; | |
| public function score(ContactInquiry $inquiry): int | |
| { | |
| $score = 0; | |
| $score += $this->scoreEmail($inquiry->email); | |
| $score += $this->scoreName($inquiry->name); | |
| $score += $this->scoreMessage($inquiry->message); | |
| $score += $this->scoreIpAddress($inquiry->ip_address); | |
| $score += $this->scoreRepeatSubmissions($inquiry); | |
| $score += $this->scoreLearnedPatterns($inquiry); | |
| return min(100, max(0, $score)); | |
| } | |
| public function scoreAndSave(ContactInquiry $inquiry): int | |
| { | |
| $score = $this->score($inquiry); | |
| $inquiry->updateQuietly([ | |
| 'spam_score' => $score, | |
| 'is_spam' => $score >= self::THRESHOLD_SPAM, | |
| ]); | |
| return $score; | |
| } | |
| public function learnFromSpamReport(ContactInquiry $inquiry): void | |
| { | |
| $emailDomain = $this->extractDomain($inquiry->email); | |
| if ($emailDomain) { | |
| SpamPattern::learn('email_domain', $emailDomain, 30); | |
| } | |
| if ($inquiry->ip_address) { | |
| SpamPattern::learn('ip_address', $inquiry->ip_address, 20); | |
| } | |
| $this->extractAndLearnPhrases($inquiry->message); | |
| } | |
| public function learnFromHamReport(ContactInquiry $inquiry): void | |
| { | |
| $emailDomain = $this->extractDomain($inquiry->email); | |
| if ($emailDomain) { | |
| $pattern = SpamPattern::where('type', 'email_domain') | |
| ->where('pattern', $emailDomain) | |
| ->first(); | |
| if ($pattern && $pattern->hit_count <= 1) { | |
| $pattern->delete(); | |
| } elseif ($pattern) { | |
| $pattern->decrement('hit_count'); | |
| } | |
| } | |
| if ($inquiry->ip_address) { | |
| $pattern = SpamPattern::where('type', 'ip_address') | |
| ->where('pattern', $inquiry->ip_address) | |
| ->first(); | |
| if ($pattern && $pattern->hit_count <= 1) { | |
| $pattern->delete(); | |
| } elseif ($pattern) { | |
| $pattern->decrement('hit_count'); | |
| } | |
| } | |
| } | |
| private function scoreEmail(string $email): int | |
| { | |
| $score = 0; | |
| $domain = $this->extractDomain($email); | |
| if (in_array($domain, self::$disposableDomains)) { | |
| $score += 40; | |
| } | |
| if (preg_match('/\d{4,}/', $email)) { | |
| $score += 10; | |
| } | |
| if (str_contains($email, '+')) { | |
| $score += 5; | |
| } | |
| $localPart = explode('@', $email)[0] ?? ''; | |
| if (strlen($localPart) > 30) { | |
| $score += 10; | |
| } | |
| if (preg_match('/^[a-z]{1,3}\d+@/i', $email)) { | |
| $score += 15; | |
| } | |
| return $score; | |
| } | |
| private function scoreName(string $name): int | |
| { | |
| $score = 0; | |
| if (preg_match('/^[a-z]+$/i', $name) && strlen($name) <= 3) { | |
| $score += 15; | |
| } | |
| if (strtoupper($name) === $name && strlen($name) > 3) { | |
| $score += 10; | |
| } | |
| if (preg_match('/[<>{}|\\\\]/', $name)) { | |
| $score += 30; | |
| } | |
| if (preg_match('/(https?:\/\/|www\.)/i', $name)) { | |
| $score += 40; | |
| } | |
| if (preg_match('/(.)\1{3,}/', $name)) { | |
| $score += 20; | |
| } | |
| return $score; | |
| } | |
| private function scoreMessage(string $message): int | |
| { | |
| $score = 0; | |
| $lowerMessage = strtolower($message); | |
| foreach (self::$spamPhrases as $phrase) { | |
| if (str_contains($lowerMessage, $phrase)) { | |
| $score += 15; | |
| } | |
| } | |
| $urlCount = preg_match_all('/https?:\/\//i', $message); | |
| if ($urlCount >= 3) { | |
| $score += 25; | |
| } elseif ($urlCount >= 1) { | |
| $score += 10; | |
| } | |
| if (preg_match_all('/[A-Z]/', $message) > strlen($message) * 0.5 && strlen($message) > 20) { | |
| $score += 15; | |
| } | |
| if (strlen($message) < 15) { | |
| $score += 10; | |
| } | |
| if (preg_match('/[\x{0400}-\x{04FF}\x{0600}-\x{06FF}\x{4E00}-\x{9FFF}]/u', $message) && !preg_match('/[\x{0400}-\x{04FF}\x{0600}-\x{06FF}\x{4E00}-\x{9FFF}]/u', '')) { | |
| $score += 5; | |
| } | |
| return min(50, $score); | |
| } | |
| private function scoreIpAddress(?string $ip): int | |
| { | |
| if (!$ip) { | |
| return 0; | |
| } | |
| $recentFromIp = ContactInquiry::where('ip_address', $ip) | |
| ->where('created_at', '>=', now()->subDay()) | |
| ->count(); | |
| if ($recentFromIp > 5) { | |
| return 30; | |
| } | |
| if ($recentFromIp > 2) { | |
| return 15; | |
| } | |
| return 0; | |
| } | |
| private function scoreRepeatSubmissions(ContactInquiry $inquiry): int | |
| { | |
| $score = 0; | |
| $sameEmail = ContactInquiry::where('email', $inquiry->email) | |
| ->where('id', '!=', $inquiry->id) | |
| ->where('created_at', '>=', now()->subDay()) | |
| ->count(); | |
| if ($sameEmail > 3) { | |
| $score += 30; | |
| } elseif ($sameEmail > 1) { | |
| $score += 15; | |
| } | |
| return $score; | |
| } | |
| private function scoreLearnedPatterns(ContactInquiry $inquiry): int | |
| { | |
| $score = 0; | |
| $domain = $this->extractDomain($inquiry->email); | |
| if ($domain) { | |
| $pattern = SpamPattern::blacklist() | |
| ->ofType('email_domain') | |
| ->where('pattern', $domain) | |
| ->first(); | |
| if ($pattern) { | |
| $score += min($pattern->weight * $pattern->hit_count, 50); | |
| } | |
| } | |
| if ($inquiry->ip_address) { | |
| $pattern = SpamPattern::blacklist() | |
| ->ofType('ip_address') | |
| ->where('pattern', $inquiry->ip_address) | |
| ->first(); | |
| if ($pattern) { | |
| $score += min($pattern->weight * $pattern->hit_count, 40); | |
| } | |
| } | |
| $phrasePatterns = SpamPattern::blacklist()->ofType('phrase')->get(); | |
| $lowerMessage = strtolower($inquiry->message); | |
| foreach ($phrasePatterns as $pattern) { | |
| if (str_contains($lowerMessage, strtolower($pattern->pattern))) { | |
| $score += min($pattern->weight * $pattern->hit_count, 30); | |
| } | |
| } | |
| return $score; | |
| } | |
| private function extractDomain(string $email): ?string | |
| { | |
| $parts = explode('@', $email); | |
| return count($parts) === 2 ? strtolower($parts[1]) : null; | |
| } | |
| private function extractAndLearnPhrases(string $message): void | |
| { | |
| $words = str_word_count(strtolower($message), 1); | |
| $wordCount = count($words); | |
| if ($wordCount < 3) { | |
| return; | |
| } | |
| for ($i = 0; $i < $wordCount - 1; $i++) { | |
| $bigram = $words[$i].' '.$words[$i + 1]; | |
| if (strlen($bigram) >= 6) { | |
| SpamPattern::learn('phrase', $bigram, 10); | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment