How LLMs Detect Spam: AI-Powered Email Classification Explained
The Problem
My Gmail inbox had 15,000 unread emails. I tried the traditional approach: filtering by sender, marking spam, creating rules. But new spam kept slipping through. Traditional filters use static rules that spammers easily evade.
Then I asked Claude Code to help me clean up my inbox. What happened next changed how I think about spam detection entirely.
Me: "How would you search for spam in my emails?"Claude: "Let me teach you about email headers and what to look for..."Claude didn’t just delete emails—it explained the signals spam leaves behind, grouped my emails by spam type, and showed me a classification system that adapts to new spam tactics.
What Traditional Spam Filters Miss
Traditional filters rely on three approaches:
- Static rule lists - Easy to evade by changing a few words
- Sender blacklists - Incomplete coverage, spammers rotate domains
- Keyword matching - False positives on legitimate emails
I’ve seen marketing emails flagged as spam while obvious phishing attempts landed in my inbox. The problem is that traditional filters don’t understand context.
LLMs change this entirely. They understand:
- Context - “This looks like a receipt you need to keep”
- Patterns - “This newsletter matches ones you marked keep”
- Value - “This promotional email has no value”
- Risk - “This could be phishing, verify sender”
How LLMs Detect Spam
Claude taught me that LLMs analyze four key signals to classify emails.
Signal 1: Email Header Analysis
Email headers contain metadata that reveals spam origins. Claude showed me which headers matter:
Received: Chain of mail servers (spoofing detection)X-Spam-Score: Pre-computed spam likelihood (if available)Return-Path: Bounce address vs From: mismatch = suspiciousX-Mailer: Spam tool signatures (bulk mailer names)DKIM/SPF/DMARC: Authentication results (failed = likely spoofed)Here’s how to extract and analyze headers:
import emailfrom email import policy
def extract_spam_signals(msg_path): """Extract spam signals from email headers""" with open(msg_path, 'rb') as f: msg = email.message_from_binary_file(f, policy=policy.default)
signals = { 'from': msg['From'], 'return_path': msg['Return-Path'], 'received_chain': msg.get_all('Received', []), 'dkim': msg.get('DKIM-Signature') is not None, 'spf': 'pass' in (msg.get('Received-SPF') or '').lower(), 'x_mailer': msg.get('X-Mailer'), 'spam_score': msg.get('X-Spam-Score', '0') }
# Check for spoofing: Return-Path differs from From if signals['return_path'] and signals['from']: from_domain = signals['from'].split('@')[-1].strip('>') return_domain = signals['return_path'].split('@')[-1].strip('>') signals['domain_mismatch'] = from_domain != return_domain
return signalsClaude identified that my spam emails often had Return-Path domains from .xyz or .top TLDs while the From address showed legitimate company names.
Signal 2: Content Pattern Recognition
LLMs detect spam by analyzing language patterns. Claude flagged these indicators:
Urgency Language: - "ACT NOW" / "Limited time" / "Don't miss out" - "Your account will be closed" - "Immediate action required"
Money Promises: - "You've won" / "Claim your prize" - "Free money" / "Guaranteed returns" - "Wire transfer" / "Bitcoin payment"
Suspicious Links: - URL shorteners (bit.ly, tinyurl) - Mismatched display text vs actual URL - Domains with typos (amaz0n.com, paypa1.com)
Unsubscribe Issues: - Missing List-Unsubscribe header - Unsubscribe link leads to different domain - Requires login to unsubscribeI asked Claude to scan my inbox for these patterns:
import re
SPAM_PATTERNS = { 'urgency': [ r'act now', r'limited time', r'don\'?t miss', r'immediate action', r'expires (today|soon)', ], 'money_promises': [ r'you\'?ve? won', r'claim your (prize|reward)', r'free money', r'guaranteed (return|profit)', ], 'suspicious_links': [ r'bit\.ly', r'tinyurl', r'click here', ]}
def scan_email_content(body): """Scan email body for spam patterns""" body_lower = body.lower() signals = {}
for category, patterns in SPAM_PATTERNS.items(): matches = [] for pattern in patterns: if re.search(pattern, body_lower): matches.append(pattern) if matches: signals[category] = matches
return signals
# Claude output for my inbox:# {# 'urgency': ['act now', 'limited time'],# 'money_promises': ['claim your prize'],# 'suspicious_links': ['bit.ly']# }Signal 3: Sender Behavior Patterns
Claude indexed my senders and found patterns I couldn’t see manually:
Spam Sender Characteristics: - High frequency: 5+ emails per week from same sender - Zero engagement: Never opened any email from them - No List-Unsubscribe header - Professional tone ratio < 20% (mostly promotional) - Domain age: Registered within last 6 monthsClaude grouped my emails by sender and computed spam scores:
from collections import defaultdictfrom datetime import datetime
def analyze_sender_behavior(emails): """Analyze sender patterns across email history""" senders = defaultdict(lambda: { 'count': 0, 'first_seen': None, 'last_seen': None, 'has_unsubscribe': False, 'opened_count': 0 })
for email in emails: sender = email['from'] sender_data = senders[sender] sender_data['count'] += 1
if 'List-Unsubscribe' in email['headers']: sender_data['has_unsubscribe'] = True
# Track engagement if email.get('opened'): sender_data['opened_count'] += 1
# Track date range date = datetime.parse(email['date']) if not sender_data['first_seen'] or date < sender_data['first_seen']: sender_data['first_seen'] = date if not sender_data['last_seen'] or date > sender_data['last_seen']: sender_data['last_seen'] = date
# Calculate spam probability for sender, data in senders.items(): engagement_rate = data['opened_count'] / data['count'] if data['count'] > 0 else 0 frequency_score = min(data['count'] / 10, 1.0) # Normalize
# High frequency + low engagement + no unsubscribe = spam data['spam_score'] = ( frequency_score * 0.3 + (1 - engagement_rate) * 0.4 + (0 if data['has_unsubscribe'] else 0.3) )
return sendersSignal 4: Contextual Classification
The real power of LLMs is contextual understanding. Claude classified my emails into categories:
Category Count Example Action------------------------------------------------Receipts 234 Keep (financial records)Newsletters 1,892 Review (some valuable)Promotions 4,567 Bulk deletePhishing 23 Delete + reportPersonal 892 KeepNotifications 567 Keep (security alerts)Spam 3,234 DeleteI asked Claude to explain its reasoning for ambiguous cases:
Email: "Your Amazon order has shipped"Claude: "This is a receipt. Contains order number, tracking link, shipping address. Legitimate domain (amazon.com), DKIM passed. Action: Keep"
Email: "ACT NOW: Your Amazon account needs verification"Claude: "This is phishing. Urgency language, link leads to amaz0n-verify.com (typosquatting), no DKIM signature. Action: Delete + Report"My Spam Detection Workflow
Claude walked me through a practical workflow for cleaning up my inbox:
+------------------+ +------------------+| Export Emails | --> | Extract Headers |+------------------+ +------------------+ | v+------------------+ +------------------+| Group by Signal | --> | LLM Classify |+------------------+ +------------------+ | v+------------------+ +------------------+| Generate Index | --> | Review & Action |+------------------+ +------------------+Step 1: Extract Email Batch
import imaplibimport email
def fetch_emails(server, user, password, limit=1000): """Fetch emails with headers and body""" mail = imaplib.IMAP4_SSL(server) mail.login(user, password) mail.select('inbox')
_, message_ids = mail.search(None, 'ALL') emails = []
for msg_id in message_ids[0].split()[:limit]: _, msg_data = mail.fetch(msg_id, '(RFC822)') raw_email = msg_data[0][1] msg = email.message_from_bytes(raw_email)
emails.append({ 'id': msg_id.decode(), 'from': msg['From'], 'subject': msg['Subject'], 'date': msg['Date'], 'headers': dict(msg.items()), 'body': get_body(msg) })
mail.logout() return emailsStep 2: Ask LLM to Identify Spam Signals
I gave Claude a batch of emails and asked:
Analyze these emails and identify:1. Spam signals in headers2. Content patterns that indicate spam3. Sender behavior anomalies4. Recommended action for eachClaude responded with a detailed breakdown:
Email ID: 1234Signals: - Return-Path: [email protected] - From: [email protected] - Domain mismatch detected - Body contains: "verify your account immediately" - 3 urgency phrases foundClassification: Phishing attemptConfidence: 95%Action: Delete and reportReason: Typosquatting domain, urgency language, no legitimate reason for Amazon to ask for verification via emailStep 3: Group-by Classification
Claude grouped my emails by spam signal type:
classification_summary = { 'headers_suspicious': 892, # DKIM failed, domain mismatch 'urgency_language': 1234, # "Act now", "Limited time" 'money_promises': 567, # "You've won", "Claim prize" 'suspicious_links': 445, # Shortened URLs, mismatched 'low_engagement': 2341, # Never opened, high frequency 'legitimate_marketing': 1892, # Has unsubscribe, moderate frequency 'personal': 892, # From known contacts 'receipts': 234, # Order confirmations, invoices}Step 4: Generate Sender Index with Spam Scores
Claude created an index I could review:
Sender Score Reason--------------------------------------------------------news@promo*.xyz 0.95 Bulk sender, no engagementdeals@marketing*.top 0.92 High frequency, urgency languagesupport@amaz0n*.com 0.98 Typosquatting, phishing[email protected] 0.25 Has unsubscribe, opened before[email protected] 0.05 Financial record, keepStep 5: Review Edge Cases Before Bulk Action
Before deleting thousands of emails, Claude flagged edge cases for manual review:
Email: "Your subscription is expiring"From: [email protected]Reason: Could be legitimate subscription you want to keepRecommendation: Check if you still use this service
Email: "Invoice #12345 attached"From: [email protected]Reason: Might be a real invoice for business expensesRecommendation: Verify vendor before deletingWhy LLM Spam Detection Works Better
The Reddit discussion that inspired this post highlighted the key advantage:
“I asked Claude how it would search for spam. It gave me a lesson on email headers and what to look for. It gave me a group-by count of my emails falling into those categories.”
Traditional filters can’t adapt. LLMs can:
| Capability | Traditional Filter | LLM Classifier |
|---|---|---|
| New spam patterns | Requires rule update | Learns from examples |
| Context understanding | None | High |
| Explainability | Rule matched | Natural language reason |
| False positive handling | Binary (spam/not) | Confidence scores |
| Phishing detection | Pattern matching | Context analysis |
Limitations to Keep in Mind
LLM spam detection isn’t perfect:
- Token cost - Analyzing thousands of emails costs money
- AI-generated spam - Spammers use LLMs too, making spam harder to detect
- False positives - Always review edge cases before bulk actions
- Not enterprise-ready - Don’t replace Gmail/Outlook spam filters for business
I use LLM classification as a second opinion, not my primary filter.
My Gmail Cleanup Results
After Claude’s analysis:
- 15,000 emails reviewed in 2 hours
- 8,234 deleted (promotions, spam, newsletters I never read)
- 23 phishing attempts identified and reported
- 6,766 kept (receipts, personal, legitimate newsletters)
The best part? Claude explained why each email was flagged, so I learned to spot spam patterns myself.
Summary
LLMs detect spam by analyzing four key signals: email headers, content patterns, sender behavior, and contextual relevance. Unlike traditional filters that rely on static rules, LLMs understand context and adapt to new spam tactics.
My workflow for inbox cleanup:
- Extract email batch with headers
- Ask LLM to identify spam signals
- Group by signal type
- Generate sender index with spam scores
- Review edge cases before bulk action
The result? A clean inbox and a better understanding of how spam actually works.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments