Build an AI Phishing Detector: 13 Steps, 90 Min [2026] - tech-insider.org
tech-insider.orgArchived Jul 18, 2026✓ Full text saved
Build an AI Phishing Detector: 13 Steps, 90 Min [2026] tech-insider.org
Full text archived locally
✦ AI Summary· Claude Sonnet
Marcus Chen
June 17, 2026
24 min read
The phishing email that used to give itself away with a typo or a clumsy logo does not exist anymore. The version landing in Canadian inboxes in 2026 reads like it was written by the person it claims to be, because a large language model trained on that person’s public writing, LinkedIn posts, and press quotes actually wrote it.
More than 90% of successful phishing attacks in 2025 used AI to mimic corporate tones closely enough to pass as internal memos, according to a security-awareness training checklist published by AwareGO. The same report cites SlashNext data showing a 3,411% jump in malicious phishing emails in 2023, right as generative AI tools went mainstream, and an FBI figure showing deepfake-related fraud reports doubled between 2023 and 2025.
The stakes go well past email. In January 2024, a finance employee at the UK engineering firm Arup joined a video call with people who looked and sounded exactly like his CFO and colleagues. Every face and voice on that call was an AI deepfake built from public video and audio of Arup executives. He authorized 15 transfers totalling HK$200 million (roughly $25 million), confirmed later by CNN and the South China Morning Post. The case is nearly two years old now, and security teams still use it to train new hires because nothing about the playbook has gone away.
This tutorial walks through building an actual working tool: a Python-based scanner that reads incoming mail, checks sender authentication, scores messages against behavioral red flags, and optionally hands suspicious text to an LLM for a second opinion. None of it replaces judgment or training. All of it gives you a concrete first line of defense you can run today, extend tomorrow, and adapt as attackers change tactics.
·
Google · Preferred Sources
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Add Now
Why This Matters More for Canadian Organizations Right Now
Canadians reported losses exceeding $704 million to fraud in 2025, according to the Canadian Anti-Fraud Centre, a record year. The CAFC itself notes that figure represents only 5 to 10% of actual incidents, since most victims never file a report at all, a detail confirmed in CAFC’s own 2025 reporting. Phishing sits at the front of a large share of that damage: it remains the most common initial attack vector across breach data IBM has tracked, and breaches that start with a phishing email run considerably more expensive to contain than ones that start any other way.
There is a Canada-specific wrinkle worth building into your own scanner from day one. A financially motivated threat actor tracked as Storm-2755 has been targeting Canadian organizations specifically through adversary-in-the-middle phishing pages, ones that sit between a victim and the real login page, capture the session cookie the moment a user enters credentials, and stay valid even after the person approves an MFA prompt. That single detail is exactly why the passkey discussion later in this guide is not a footnote: a stolen password-plus-MFA-code combination is still useful to an attacker running this kind of attack, while a passkey tied to a physical device is not.
None of this means Canadian businesses face a fundamentally different threat than anyone else. It means the cost of skipping authentication checks, ignoring channel-switch requests, or leaving MFA as your only account-takeover defense is measurably higher this year than it was two years ago, with real dollar figures from a federal agency behind that statement rather than a vendor’s marketing claim.
What You Will Build in This Tutorial
By the end of this guide you will have a scanner called phishing-sentinel that connects to a mailbox over IMAP, pulls unread messages, and runs each one through three independent checks before deciding whether to raise an alert.
Architecture at a Glance
Authentication layer: verifies SPF, DKIM, and DMARC records for the sending domain using DNS lookups
Heuristic layer: scores urgency language, tonal formality, and link/domain mismatches with plain Python, no external calls
LLM layer (optional): sends flagged text to a language model to catch contextual hallucinations that rules miss
Alerting layer: logs every scan and posts a webhook message for anything over your risk threshold
Each layer is independent. You can run the tool with just the authentication and heuristic layers and add the LLM classifier later once you have measured false-positive rates. That staged approach matters more than it sounds: a rushed all-in build is exactly the kind of thing that produces alert fatigue in week one.
Prerequisites and Requirements
You do not need a security engineering background to follow along, but you should be comfortable running Python scripts from a terminal and editing a config file. Here is everything the project touches.
Requirement Version / Detail Why You Need It
Python 3.11 or newer Runtime for every script in this tutorial
pip Latest, bundled with Python Installs the packages below
IMAP mailbox access Gmail, Outlook, or any IMAP4 provider with app-password support Lets the scanner read incoming mail
imap-tools Latest release via pip Wraps Python’s imaplib with a much friendlier API
dnspython & checkdmarc Latest release via pip Validates SPF, DKIM, and DMARC DNS records
anthropic (optional) Latest release via pip Powers the LLM contextual-analysis layer
Webhook URL (optional) Slack, Microsoft Teams, or Discord incoming webhook Delivers real-time alerts for flagged mail
One honest caveat before you start: if you are not sure a package version is current, the safest move is to install without pinning and let pip resolve the latest release, then check the changelog if something behaves unexpectedly. This tutorial deliberately avoids hardcoding version numbers you would have to verify yourself.
A Note on Privacy Before You Deploy This
A tool that reads mailbox content, even to protect the people using it, needs a clear scope before it goes live, not after. Three things are worth settling before Step 1.
Tell employees the monitoring exists, what it looks for, and what it does not do. A scanner introduced quietly, then discovered later, damages trust in a way that outweighs the security benefit.
Scope it to what the task requires. This tutorial reads subject lines and message bodies to score risk. It has no reason to archive full message content indefinitely, and neither does your deployment.
Set a retention window on the log file and decide who can read it. The JSON log in Step 9 is genuinely useful for tuning thresholds and for the PIPEDA-related recordkeeping mentioned later in the FAQ, but “useful” is not the same as “keep forever.”
None of this is a compliance footnote you can skip. It is the difference between a tool your organization trusts enough to keep running and one that gets quietly disabled the first time someone asks an uncomfortable question about who can read their email.
Step 1: Set Up the Project and Install Dependencies
Create a project folder and a virtual environment first, so the packages below do not collide with anything else on your machine.
mkdir phishing-sentinel && cd phishing-sentinel
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
touch requirements.txt fetcher.py auth_check.py heuristics.py link_check.py llm_classifier.py alerts.py main.py .env
Add the dependencies to requirements.txt. Leaving versions unpinned is intentional here. Pin them yourself once you have a working baseline and want reproducible deploys.
imap-tools
dnspython
checkdmarc
requests
python-dotenv
anthropic
Install everything with a single command.
pip install -r requirements.txt
Step 2: Configure Secure Mailbox Access
Never hardcode a mailbox password in a script you might commit to a repository. Create a .env file instead and load it at runtime. If you use Gmail or Microsoft 365, generate an app-specific password rather than using your normal login, since both providers block plain password IMAP logins by default for security reasons.
IMAP_HOST=imap.gmail.com
IMAP_USER=your-address@example.com
IMAP_APP_PASSWORD=your-16-character-app-password
ANTHROPIC_API_KEY=your-anthropic-key-if-using-the-llm-layer
ALERT_WEBHOOK_URL=https://hooks.slack.com/services/your/webhook/url
Add .env to your .gitignore immediately, before you forget and commit it. This is one of the most common ways teams accidentally leak mailbox credentials into public repositories.
Step 3: Build the IMAP Email Fetcher
The imap-tools library wraps Python’s built-in imaplib module with a much cleaner interface for filtering and reading messages. This function pulls unseen messages without marking them read, so your normal mail client behavior stays untouched.
import os
from imap_tools import MailBox, AND
from dotenv import load_dotenv
load_dotenv()
IMAP_HOST = os.getenv("IMAP_HOST", "imap.gmail.com")
IMAP_USER = os.getenv("IMAP_USER")
IMAP_PASS = os.getenv("IMAP_APP_PASSWORD")
def fetch_unseen_messages(limit=25):
messages = []
with MailBox(IMAP_HOST).login(IMAP_USER, IMAP_PASS, initial_folder="INBOX") as mailbox:
for msg in mailbox.fetch(AND(seen=False), limit=limit, mark_seen=False):
messages.append({
"uid": msg.uid,
"subject": msg.subject,
"from_addr": msg.from_,
"reply_to": msg.headers.get("reply-to", ("",))[0],
"date": msg.date,
"text": msg.text or msg.html or "",
})
return messages
Keep the fetch limit low while you are testing. Scanning an entire backlog on your first run makes it much harder to tell which finding came from which change you just made.
If your organization enforces conditional access or has disabled basic authentication entirely, imap-tools also supports OAuth2 login flows for Microsoft 365 and Gmail workspace accounts, at the cost of a slightly longer setup involving an app registration in your identity provider. The app-password route above is the faster path for a personal mailbox or a small business account without those policies in place.
Step 4: Parse Headers and Extract Sender Identity
The single most useful, most overlooked signal in a phishing email is the gap between the display name and the actual address behind it. “IT Support” showing up from it-support@corp-secure-updates.net instead of your real domain is a five-second check that catches a large share of impersonation attempts before any scoring model runs.
import re
def extract_domain(email_address: str) -> str:
match = re.search(r"@([\w.-]+)$", email_address or "")
return match.group(1).lower() if match else ""
def display_name_mismatch(subject: str, from_addr: str, known_domains: list) -> bool:
domain = extract_domain(from_addr)
return bool(domain) and domain not in known_domains
This check alone will not catch a well-run campaign, since attackers register lookalike domains constantly. Pair it with the authentication check in the next step, which looks at DNS records instead of the address string.
Step 5: Verify SPF, DKIM, and DMARC Authentication
If you have not set up email authentication for your own domain yet, our dedicated DMARC, SPF, and DKIM setup guide covers that from scratch. Here, we are using the checkdmarc package to check whether an incoming message’s sending domain has valid authentication records at all.
import checkdmarc
def check_domain_authentication(sender_domain: str) -> dict:
result = {"spf_valid": None, "dmarc_valid": None, "dmarc_policy": None, "error": None}
if not sender_domain:
result["error"] = "no domain to check"
return result
try:
report = checkdmarc.check_domains([sender_domain], parked=False)[0]
result["spf_valid"] = report.get("spf", {}).get("valid")
result["dmarc_valid"] = report.get("dmarc", {}).get("valid")
result["dmarc_policy"] = report.get("dmarc", {}).get("tags", {}).get("p", {}).get("value")
except Exception as exc:
result["error"] = str(exc)
return result
One important limitation to hold onto: authentication passing means the domain is who it says it is, not that the sender is trustworthy. A compromised legitimate mailbox, or a newly registered lookalike domain with its own valid DMARC record, sails through this check every time. Treat it as one signal among several, never the whole verdict.
Step 6: Build the Heuristic Red-Flag Scoring Engine
This is the layer that runs entirely on your own CPU, with no external calls and no per-message cost. It looks for the two patterns that show up again and again in the research on AI-generated social engineering: manufactured urgency, and a formality level that reads as slightly too polished for how colleagues actually write to each other.
import re
URGENCY_PATTERNS = [
r"\bact now\b", r"\bimmediately\b", r"\bwithin (the )?(next )?\d+ (hours|minutes)\b",
r"\bverify your account\b", r"\bsuspended\b", r"\bwire transfer\b", r"\bgift card\b",
r"\bfinal notice\b", r"\bconfidential\b.*\brequest\b",
]
def urgency_score(text: str) -> float:
text_lower = text.lower()
hits = sum(1 for pattern in URGENCY_PATTERNS if re.search(pattern, text_lower))
return min(hits / 3, 1.0)
def formality_score(text: str) -> float:
contractions = len(re.findall(r"\b\w+'(t|re|ve|ll|d|s)\b", text, re.IGNORECASE))
sentences = max(text.count(".") + text.count("!") + text.count("?"), 1)
words = max(len(text.split()), 1)
avg_sentence_len = words / sentences
score = 0.0
if (contractions / sentences) < 0.05:
score += 0.5
if avg_sentence_len > 22:
score += 0.3
if not re.search(r"\b(hey|hi|thanks|cheers|btw)\b", text.lower()):
score += 0.2
return min(score, 1.0)
Expect to tune URGENCY_PATTERNS for your own organization within the first week. A hospital, a law firm, and a game studio all get impersonated with different vocabulary, and a keyword list built for one will miss obvious attempts aimed at another.
The formality check deserves a word of caution. It is deliberately blunt: a genuinely careful colleague who writes without contractions is not a phishing attempt, and this function alone will misjudge them. That is exactly why it contributes only twenty points toward the final risk score in Step 10 rather than triggering an alert on its own. No single heuristic here is meant to stand alone.
Step 7: Detect Sender and Link Domain Mismatches
AI-written phishing text is clean, but the infrastructure behind it usually is not. Attackers still need a domain to host a credential-harvesting page, and that domain rarely matches the brand it is impersonating.
from urllib.parse import urlparse
import re
def extract_links(text: str) -> list:
return re.findall(r'https?://[^\s<>"\']+', text)
def link_domain_mismatch(links: list, expected_domains: list) -> list:
suspicious = []
for link in links:
netloc = urlparse(link).netloc.lower()
if not any(netloc == d or netloc.endswith("." + d) for d in expected_domains):
suspicious.append(link)
return suspicious
Maintain expected_domains as an allowlist of your own corporate domains plus any vendors you legitimately link to, such as your payroll provider or benefits portal. Anything outside that list in a message asking for credentials or payment deserves a second look.
Step 8: Add an LLM Classification Layer for Contextual Hallucinations
Heuristics catch known patterns. What they miss is the message that gets everything else right, correct project name, correct tone, correct signature block, but includes one detail that would not actually be true. That is where a language model earns its cost: reading for internal consistency the way a sharp human reviewer would.
import os
import json
from anthropic import Anthropic
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
CLASSIFY_PROMPT = """You are a phishing-detection assistant. Read the email body below and return \
ONLY a JSON object with two fields: "ai_generated_likelihood" (integer 0-100) and "reasoning" \
(one sentence). Flag hallmarks of AI-generated social engineering: unnatural formality, contextual \
details that do not quite fit, hyper-specific urgency, or a request that switches communication channels.
EMAIL BODY:
{body}
"""
def classify_email(body: str) -> dict:
if not body.strip():
return {"ai_generated_likelihood": 0, "reasoning": "empty body"}
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=200,
messages=[{"role": "user", "content": CLASSIFY_PROMPT.format(body=body[:4000])}],
)
raw = message.content[0].text
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"ai_generated_likelihood": None, "reasoning": "unparsable model output"}
Two practical notes. First, a fast, inexpensive model is the right choice here since this runs on every flagged message. Save a larger model for cases your team escalates for deeper review. Second, free-text JSON parsing works for a tutorial but turns fragile in production. The Advanced Tips section below covers a sturdier approach.
Step 9: Build the Alerting and Logging Module
Every scan should leave a paper trail, whether or not it triggers an alert. That log is what lets you retune thresholds later instead of guessing.
import json
import logging
from datetime import datetime, timezone
import requests
logging.basicConfig(
filename="phishing_scan.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
def log_finding(finding: dict) -> None:
finding["scanned_at"] = datetime.now(timezone.utc).isoformat()
logging.info(json.dumps(finding))
def send_alert(finding: dict, webhook_url: str) -> None:
if not webhook_url:
return
text = (
f":rotating_light: Suspicious email flagged\n"
f"Subject: {finding['subject']}\n"
f"From: {finding['from_addr']}\n"
f"Risk score: {finding['risk_score']}/100\n"
f"Reasons: {', '.join(finding['reasons']) or 'none listed'}"
)
requests.post(webhook_url, json={"text": text}, timeout=5)
The webhook payload above uses Slack’s incoming-webhook format, which Microsoft Teams and Discord both accept with minor adjustments to the JSON keys. Keep the timeout short so a slow or unreachable webhook never stalls the whole scan.
Step 10: Wire Everything Together in main.py
This is where every earlier module gets combined into a single scoring pipeline, then run against your mailbox.
import os
from dotenv import load_dotenv
from fetcher import fetch_unseen_messages
from auth_check import check_domain_authentication
from heuristics import urgency_score, formality_score
from link_check import extract_links, link_domain_mismatch
from llm_classifier import classify_email
from alerts import log_finding, send_alert
load_dotenv()
WEBHOOK_URL = os.getenv("ALERT_WEBHOOK_URL")
TRUSTED_LINK_DOMAINS = ["tech-insider.org", "microsoft.com", "google.com"]
RISK_THRESHOLD = 55
def score_message(msg: dict) -> dict:
domain = (msg["from_addr"] or "").split("@")[-1]
auth = check_domain_authentication(domain)
text = msg["text"] or ""
u_score = urgency_score(text)
f_score = formality_score(text)
links = extract_links(text)
bad_links = link_domain_mismatch(links, TRUSTED_LINK_DOMAINS)
llm_result = classify_email(text)
risk_score = round(
(u_score * 25)
+ (f_score * 20)
+ (25 if bad_links else 0)
+ ((llm_result.get("ai_generated_likelihood") or 0) * 0.3)
)
reasons = []
if u_score > 0.3:
reasons.append("urgency language")
if f_score > 0.5:
reasons.append("unnatural formality")
if bad_links:
reasons.append("mismatched link domain")
if auth.get("dmarc_valid") is False:
reasons.append("failed DMARC")
return {
"subject": msg["subject"], "from_addr": msg["from_addr"],
"risk_score": risk_score, "reasons": reasons,
"auth": auth, "llm": llm_result,
}
def run_scan():
messages = fetch_unseen_messages(limit=25)
flagged = 0
for msg in messages:
finding = score_message(msg)
log_finding(finding)
if finding["risk_score"] >= RISK_THRESHOLD:
flagged += 1
send_alert(finding, WEBHOOK_URL)
print(f"Scanned {len(messages)} messages, flagged {flagged}.")
if __name__ == "__main__":
run_scan()
The risk weights above (25/20/25/0.3) are starting defaults, not settled science. Run against a week of your own mail, note where the scanner agreed and disagreed with your own judgment, and adjust from there.
Step 11: Run Your First Scan and Read the Output
With the .env file filled in and dependencies installed, run the scanner directly from your terminal.
$ python main.py
Scanned 25 messages, flagged 2.
Check phishing_scan.log to see the full detail behind that summary line.
2026-06-17T14:02:11+00:00 INFO {"subject": "URGENT: Wire confirmation needed before 5pm", "from_addr": "ceo-office@corp-updates-secure.com", "risk_score": 78, "reasons": ["urgency language", "unnatural formality", "mismatched link domain"], "auth": {"spf_valid": false, "dmarc_valid": false, "dmarc_policy": "none"}}
2026-06-17T14:02:13+00:00 INFO {"subject": "Q2 planning notes", "from_addr": "jsmith@yourcompany.com", "risk_score": 6, "reasons": [], "auth": {"spf_valid": true, "dmarc_valid": true, "dmarc_policy": "reject"}}
The first entry shows exactly the pattern this tool is built to catch: failed authentication, manufactured urgency, and a link pointing somewhere it should not. The second is an ordinary internal email that clears every check.
Step 12: Schedule Continuous Monitoring
A scanner that only runs when you remember to run it protects nobody. Schedule it with cron on Linux or macOS.
# crontab -e
*/15 * * * * cd /opt/phishing-sentinel && /opt/phishing-sentinel/venv/bin/python main.py >> /var/log/phishing-sentinel.log 2>&1
On systems using systemd, a timer unit gives you better logging and restart behavior than cron.
# /etc/systemd/system/phishing-sentinel.timer
[Unit]
Description=Run AI phishing sentinel every 15 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
Fifteen minutes is a reasonable starting interval for a small business mailbox. High-volume shared inboxes may need five-minute intervals, which pushes API costs higher on the LLM layer, so budget accordingly.
Step 13: Test Against Sample Phishing Fixtures
Before pointing this at a live mailbox, confirm the scoring logic behaves the way you expect against known samples.
# test_heuristics.py
from heuristics import urgency_score, formality_score
SAMPLE_PHISH = (
"Dear colleague, this is a time-sensitive request. Please process the wire transfer "
"immediately and confirm within 30 minutes. This matter is confidential."
)
def test_urgency_detects_high_pressure_language():
assert urgency_score(SAMPLE_PHISH) > 0.5
def test_formality_flags_stiff_tone():
assert formality_score(SAMPLE_PHISH) > 0.5
$ pytest test_heuristics.py -v
Keep a small library of real (redacted) phishing samples your organization has received in the past. They make far better regression tests than anything invented from scratch, because they reflect the wording attackers actually use against your specific industry.
The Complete Working Project
Every file from the steps above combines into one working tool. Here is the full project layout.
phishing-sentinel/
├── .env
├── requirements.txt
├── fetcher.py # Step 3: IMAP message retrieval
├── auth_check.py # Step 5: SPF/DKIM/DMARC verification
├── heuristics.py # Step 6: urgency and formality scoring
├── link_check.py # Step 7: link/domain mismatch detection
├── llm_classifier.py # Step 8: LLM contextual analysis
├── alerts.py # Step 9: logging and webhook alerts
├── main.py # Step 10: orchestration and scoring
└── test_heuristics.py # Step 13: regression tests
Nine files, none longer than about 60 lines, and no dependency you cannot explain in one sentence. That is deliberate: a security tool nobody on your team can read and modify is a tool that quietly rots the first time an attacker changes their wording.
Extending the Project: Ideas for Round Two
Once the base pipeline runs reliably for a week or two, a few additions tend to pay off quickly.
Swap the flat log file for SQLite. A single-table database with columns for subject, sender, risk score, and analyst verdict makes the precision and recall tracking from the KPI section far easier than grepping a text log.
Add a minimal review dashboard. Even a small Flask page listing the last 50 flagged messages with an approve/reject button turns “read the log file” into something a non-technical analyst can actually use day to day.
Route high-risk findings into your ticketing system. A webhook into Jira or ServiceNow instead of, or alongside, Slack gives you an auditable queue instead of a channel that scrolls messages out of view.
Scan attachments separately. This tutorial only reads message text. Macro-enabled documents and disguised executables need their own check, ideally routed through a sandboxed detonation service rather than opened directly.
Add sender reputation history. Track how often a given domain has shown up in past flagged messages. A domain that appears for the first time today, sending an urgent wire request, deserves more suspicion than one your finance team has exchanged mail with for three years.
None of these are required to get value from the tool as built above. They are the natural next steps once you have real data showing where the base version falls short for your own mail traffic.
Common Pitfalls When Building AI Phishing Detection
Trusting a DMARC pass as proof of safety: authentication confirms the domain, not the intent. A compromised legitimate account clears every check in Step 5.
Building an English-only keyword list: Canadian inboxes see phishing in both English and French, and a urgency-pattern list built from one language misses half the attempts aimed at the other.
Sending every message through the LLM layer: costs scale with volume fast. Gate LLM calls behind the free heuristic layer so only borderline cases reach Step 8.
Hardcoding credentials during testing “just for now”: that test script ends up in a shared repository more often than anyone intends. Use the .env file from the first commit.
Ignoring API rate limits: hammering an IMAP server or an LLM API without backoff logic gets you throttled or temporarily locked out at the worst possible time.
Treating the tool as finished after Step 13: attackers adjust wording monthly. A scanner nobody revisits degrades quietly until someone notices it missed something obvious.
Troubleshooting Guide
Nine issues come up often enough during setup that they are worth checking first.
Symptom Likely Cause Fix
IMAP login fails with an authentication error Provider blocks plain password logins Generate an app-specific password, or switch to OAuth2 if your provider requires it
checkdmarc raises a DNS timeout Local resolver is slow, rate-limited, or offline Add retry/backoff logic, or point resolution at a public resolver such as 1.1.1.1
Every message scores near zero Heuristic keyword list is too narrow for your organization’s language Expand URGENCY_PATTERNS with terms specific to your industry and region
LLM classifier returns unparsable output The model wrapped the JSON in conversational text Use structured output or tool calling instead of free-text parsing
Script hangs on large mailboxes Fetch limit too high on the first run Lower the limit in fetch_unseen_messages and paginate with UID ranges
Webhook alerts never arrive Missing, expired, or firewalled webhook URL Test the webhook independently with a manual curl request before blaming the scanner
False positives on legitimate newsletters Marketing email naturally scores high on urgency and link density Add a sender allowlist that is checked before scoring runs
Cron job runs but nothing happens Environment variables from .env are not loaded outside your shell session Load .env explicitly inside the script, or export variables directly in the crontab
DMARC passes but the email still turns out to be fake DMARC proves domain authenticity, not sender trustworthiness Never rely on one signal alone. Combine authentication, heuristic, and LLM scoring together
Advanced Tips: Cutting False Positives and API Costs
A few refinements separate a weekend project from something you would actually trust running unattended.
Cache the static part of your prompt. The instruction block in Step 8 never changes, only the email body does. Prompt caching on repeated system instructions cuts the token cost of high-volume scanning significantly.
Gate expensive calls behind cheap ones. Only send messages that clear a low heuristic threshold to the LLM layer, rather than scoring every single message with it.
Log analyst verdicts, not just scanner output. When a human reviews a flagged message, record whether it was a true or false positive, then retune thresholds against that record monthly.
Pair this with out-of-band verification for money or credential requests, regardless of risk score. No scoring system should be the only gate in front of a wire transfer.
Forward JSON log lines into your existing SIEM. If you already run Wazuh or a Graylog instance, correlating phishing-sentinel findings against login anomalies and other security events catches campaigns a single tool would miss on its own.
Beyond Email: Verifying Deepfake Voice and Video Requests
Everything above protects an inbox. It does nothing for a phone call or a video conference, and that is exactly where the Arup case landed. Voice cloning now needs only a few seconds of audio, easily pulled from a public talk, a podcast appearance, or a company town hall recording, to produce a convincing fake.
This is worth internalizing before you consider the scanner “done.” The finance employee in the Arup case was not careless. He initially suspected a phishing attempt from the tone of the first message, raised that doubt, and was talked past it because every face and voice on the follow-up video call matched people he recognized. Software cannot fix a failure mode like that. Only a verification habit, checked every time regardless of how convincing the request looks, closes the gap.
Out-of-Band Verification Protocols
The single rule that would have stopped the Arup transfer: verify any request involving money, credentials, or account changes through a channel you already trust, using a phone number you already have on file, never one supplied in the message itself. If your CEO normally messages you on Slack and suddenly emails an urgent wire request, the channel switch alone is worth a phone call before anything else happens.
Why Passkeys Blunt Credential Phishing
On the account-takeover side of this problem, passkeys built on the FIDO2/WebAuthn standard remove the shared secret entirely. A cryptographic credential tied to your device cannot be phished the way a password or a one-time code can, because there is no secret in transit for an attacker to intercept or trick you into typing on a lookalike site.
AI Phishing vs Legacy Phishing at a Glance
Security awareness training built around the old playbook increasingly points people the wrong way. Here is what actually changed.
Indicator Legacy Phishing (Pre-2023) AI-Generated Phishing (2026)
Spelling and grammar Frequent errors, awkward phrasing Clean, often grammatically flawless
Personalization Generic greeting, mass-blasted Hyper-specific details pulled from public and OSINT sources
Tone Often mismatched or oddly translated Matches internal corporate style closely
Urgency Blunt and generic (“verify now”) Layered and contextual, referencing real deadlines or projects
Delivery Single channel, static content Multi-channel: email, voice, and SMS combined
Best defense Spam filters, spelling red flags Out-of-band verification, passkeys, and behavioral heuristics
Choosing Between Detection Layers
Detection Layer What It Catches Relative Cost Key Limitation
SPF / DKIM / DMARC Spoofed domains, unauthorized mail servers Free (DNS lookups only) Blind to compromised accounts and new lookalike domains with valid records
Heuristic scoring Urgency language, tonal formality, link/domain mismatches Free (local CPU) Rule-based, needs regular tuning as wording shifts
LLM classification Contextual hallucinations, hyper-personalized pretexting Per-token API cost Can misjudge unusual but legitimate writing styles
Measuring Success: KPIs to Track Once This Is Running
A scanner that runs quietly without anyone checking its output is not much better than no scanner at all. Track these numbers from week one, ideally in a simple spreadsheet fed by your JSON log, so tuning decisions rest on data rather than gut feeling.
Precision: of everything flagged, what share was a genuine attempt, versus a legitimate email that scored too high? Low precision means your thresholds or keyword list need tightening.
Recall against known samples: run last month’s confirmed phishing reports back through the scanner regularly. Anything it would have missed tells you exactly where the heuristics or LLM prompt need work.
Time to alert: the gap between a message landing and a webhook firing. Fifteen-minute cron intervals mean a worst-case delay of nearly fifteen minutes, which may be too slow for a high-risk shared inbox.
Cost per scan: track LLM API spend against message volume monthly. A sudden spike usually means the heuristic gate in Step 8 is not filtering enough traffic before the expensive call.
Analyst override rate: how often a human reviewer disagrees with the scanner’s verdict. A high override rate in either direction is the clearest signal that your risk weights from Step 10 need revisiting.
None of these numbers matter in isolation. A scanner with perfect precision and terrible recall is quietly letting real attempts through. One with perfect recall and terrible precision is training your team to ignore its alerts entirely. Track both, every month, and adjust the weights in score_message() accordingly.
Frequently Asked Questions
Can AI phishing detection tools replace employee security training?
No. A scanner catches what it is built to catch. Training builds the judgment to handle everything else, including the phone call or video request this tool cannot see at all. Treat the two as complementary layers, not substitutes.
Do I need an Anthropic API key to follow this tutorial?
Only for Step 8. The authentication and heuristic layers in Steps 5 through 7 work entirely on their own and catch a meaningful share of attempts without any external API or per-message cost.
Will this scanner work with Gmail and Microsoft 365?
Yes, both support IMAP with app-specific passwords or OAuth2, though Microsoft 365 tenants with strict conditional-access policies may require registering an app in Azure AD instead of using a simple app password.
How much does the LLM classification layer cost to run?
It depends entirely on message volume and which model you choose. Gating LLM calls behind the free heuristic layer, as described in the Advanced Tips section, is the single biggest cost lever available to you.
Does passing SPF, DKIM, and DMARC mean an email is safe?
No. It means the sending domain is authenticated, nothing more. A compromised legitimate mailbox or a freshly registered lookalike domain with its own valid DMARC record both pass this check without issue.
Can this same approach be adapted for Slack or Teams messages?
Yes. The heuristic and LLM layers do not care where text comes from. Swap the IMAP fetcher in Step 3 for the relevant chat platform API and the rest of the pipeline works unchanged.
How do I reduce false positives on legitimate newsletters?
Add a sender allowlist checked before scoring begins, as noted in the troubleshooting table above. Marketing email naturally trips urgency and link-density heuristics even when it is entirely legitimate.
Do Canadian organizations have legal obligations after a phishing-related breach?
If personal information is compromised and the incident creates a real risk of significant harm, PIPEDA requires notifying both the Office of the Privacy Commissioner and the affected individuals. That legal backdrop is one more reason to keep the detailed JSON log from Step 9 around. It gives you a timestamped record of what was flagged, and when, if you ever need to reconstruct a timeline for a regulator.
Is a custom scanner better than buying a commercial anti-phishing product?
Not necessarily, and that is not really the point of this tutorial. Commercial tools bring dedicated threat-intel feeds and support contracts a weekend project cannot match. Building your own teaches you exactly what your organization’s mail traffic actually looks like, which makes evaluating a commercial product afterward a far more informed decision.
Related Coverage
DMARC, SPF & DKIM Setup: Stop Spoofing in 12 Steps [2026]
How to Spot Phishing Scams in 2026: Red Flags
Wazuh SIEM Setup: Free SOC in 12 Steps, 45 Min [2026]
Suricata IDS/IPS Setup in 12 Steps, 40 Min [2026]
Ransomware in 2026: How Canadians Stay Protected
Keycloak SSO: Self-Host IAM in 12 Steps, 40 Min [2026]
More Cybersecurity Coverage
Marcus Chen
GAMING & CONSUMER TECH EDITOR
Marcus Chen is a senior editor at Tech Insider, where he leads coverage of the US online gaming market, including sweepstakes and social casinos, alongside consumer technology. He evaluates operators on their published terms, licensing and RNG certifications, stated redemption policies, and corroborating independent reporting, and writes plainly about what the evidence supports. Tech Insider does not run first-party money tests and does not gamble with reader funds. Marcus has reported on the technology and online-gaming industries for more than a decade.
View all articles