Screen Emails for Phishing
Step-by-step tutorial for building an email phishing screening service with Node.js using the ScamVerify™ API.
This tutorial walks you through building an email screening service in Node.js that detects phishing, spoofing, and fraud. You will parse raw emails (including headers), call the ScamVerify™ API, and automatically quarantine high-risk messages.
Prerequisites
- Node.js 18 or later
- A ScamVerify™ API key (get one at scamverify.ai/settings/api)
- Basic understanding of email headers (SPF, DKIM, DMARC)
What You Will Build
An email screening pipeline that:
- Receives a raw email with headers
- Calls the ScamVerify™ email analysis API
- Interprets the header analysis (SPF, DKIM, DMARC checks)
- Checks embedded URLs and phone numbers for threats
- Auto-quarantines high-risk emails
- Returns a detailed screening report
Set up the project
mkdir email-screener && cd email-screener
npm init -y
npm install express dotenv mailparserCreate a .env file:
SCAMVERIFY_API_KEY=sv_live_your_key_here
PORT=3000Create the ScamVerify™ email client
Build a client module focused on email analysis.
// scamverify.js
class ScamVerifyEmailClient {
constructor(apiKey) {
if (!apiKey || !apiKey.startsWith('sv_')) {
throw new Error('Invalid API key. Must start with sv_live_ or sv_test_');
}
this.apiKey = apiKey;
this.baseUrl = 'https://scamverify.ai/api/v1';
}
async analyzeEmail(emailBody, rawHeaders = null) {
const payload = { email_body: emailBody };
// Including raw headers enables SPF/DKIM/DMARC analysis
if (rawHeaders) {
payload.raw_headers = rawHeaders;
}
const response = await fetch(`${this.baseUrl}/email/analyze`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorMessages = {
401: 'Invalid or revoked API key',
402: 'Quota exhausted',
429: 'Rate limited',
};
throw new Error(errorMessages[response.status] || `API error: ${response.status}`);
}
return response.json();
}
}
module.exports = { ScamVerifyEmailClient };Build the email parser
Extract the body and headers from a raw email. The raw_headers parameter is what enables ScamVerify™ to check SPF, DKIM, and DMARC records.
// email-parser.js
const { simpleParser } = require('mailparser');
async function parseRawEmail(rawEmail) {
const parsed = await simpleParser(rawEmail);
// Extract the raw headers (everything before the first blank line)
const headerEndIndex = rawEmail.indexOf('\r\n\r\n');
const fallbackIndex = rawEmail.indexOf('\n\n');
const splitIndex = headerEndIndex !== -1 ? headerEndIndex : fallbackIndex;
const rawHeaders = splitIndex !== -1
? rawEmail.substring(0, splitIndex)
: null;
return {
from: parsed.from?.text || '',
to: parsed.to?.text || '',
subject: parsed.subject || '',
body: parsed.text || parsed.html || '',
rawHeaders,
date: parsed.date,
attachments: parsed.attachments?.length || 0,
};
}
module.exports = { parseRawEmail };Build the screening service
Create the main screening logic that interprets ScamVerify™ results and makes quarantine decisions.
// screener.js
// Risk thresholds for auto-quarantine
const QUARANTINE_SCORE = 60;
const REVIEW_SCORE = 35;
function classifyEmail(apiResult, parsedEmail) {
const { risk_score, verdict, explanation, signals } = apiResult;
const report = {
riskScore: risk_score,
verdict,
explanation,
from: parsedEmail.from,
subject: parsedEmail.subject,
action: 'deliver', // default
flags: [],
};
// Check header authentication
if (signals.header_analysis) {
const headers = signals.header_analysis;
if (headers.spf === 'fail') {
report.flags.push('SPF authentication failed. Sender domain may be spoofed.');
}
if (headers.dkim === 'fail') {
report.flags.push('DKIM signature invalid. Message may have been tampered with.');
}
if (headers.dmarc === 'fail') {
report.flags.push('DMARC policy failure. Domain alignment issue detected.');
}
if (headers.return_path_mismatch) {
report.flags.push('Return path does not match sender domain. Possible spoofing.');
}
}
// Check for brand impersonation
if (signals.brand_impersonation) {
report.flags.push(`Brand impersonation detected: ${signals.brand_impersonation}`);
}
// Check embedded URLs
const maliciousUrls = (signals.extracted_urls || [])
.filter(u => u.risk_score >= 50);
if (maliciousUrls.length > 0) {
report.flags.push(
`${maliciousUrls.length} malicious URL(s) found in email body.`
);
report.maliciousUrls = maliciousUrls.map(u => u.url);
}
// Check embedded phone numbers
const riskyPhones = (signals.extracted_phones || [])
.filter(p => p.risk_score >= 50);
if (riskyPhones.length > 0) {
report.flags.push(
`${riskyPhones.length} suspicious phone number(s) found in email body.`
);
}
// Determine action
if (risk_score >= QUARANTINE_SCORE) {
report.action = 'quarantine';
} else if (risk_score >= REVIEW_SCORE || report.flags.length >= 2) {
report.action = 'review';
}
return report;
}
module.exports = { classifyEmail, QUARANTINE_SCORE, REVIEW_SCORE };Wire it all together in Express
Create the server with endpoints for screening raw emails and viewing quarantine status.
// server.js
require('dotenv').config();
const express = require('express');
const { ScamVerifyEmailClient } = require('./scamverify');
const { parseRawEmail } = require('./email-parser');
const { classifyEmail } = require('./screener');
const app = express();
app.use(express.json({ limit: '5mb' })); // Emails can be large
app.use(express.text({ type: 'message/rfc822', limit: '5mb' }));
const client = new ScamVerifyEmailClient(process.env.SCAMVERIFY_API_KEY);
// In-memory store for demo purposes (use a database in production)
const quarantine = [];
const reviewQueue = [];
// POST /screen - Screen a raw email
app.post('/screen', async (req, res) => {
let emailBody, rawHeaders;
if (typeof req.body === 'string') {
// Raw email submitted as text
const parsed = await parseRawEmail(req.body);
emailBody = parsed.body;
rawHeaders = parsed.rawHeaders;
} else {
// JSON with email_body and optional raw_headers
emailBody = req.body.email_body;
rawHeaders = req.body.raw_headers;
}
if (!emailBody) {
return res.status(400).json({
error: 'Missing email content',
hint: 'Send raw email as text/plain or JSON with email_body field',
});
}
try {
// Call ScamVerify API
const apiResult = await client.analyzeEmail(emailBody, rawHeaders);
// Classify and determine action
const parsed = await parseRawEmail(
rawHeaders ? `${rawHeaders}\r\n\r\n${emailBody}` : emailBody
);
const report = classifyEmail(apiResult, parsed);
// Execute the action
if (report.action === 'quarantine') {
quarantine.push({ ...report, timestamp: new Date().toISOString() });
console.log(`QUARANTINED: ${parsed.subject} from ${parsed.from}`);
} else if (report.action === 'review') {
reviewQueue.push({ ...report, timestamp: new Date().toISOString() });
console.log(`FLAGGED FOR REVIEW: ${parsed.subject} from ${parsed.from}`);
}
return res.json({
success: true,
action: report.action,
riskScore: report.riskScore,
verdict: report.verdict,
explanation: report.explanation,
flags: report.flags,
});
} catch (error) {
console.error('Email screening failed:', error.message);
return res.status(500).json({ error: 'Screening failed. ' + error.message });
}
});
// POST /screen/json - Screen with pre-parsed email body
app.post('/screen/json', async (req, res) => {
const { email_body, raw_headers } = req.body;
if (!email_body) {
return res.status(400).json({ error: 'Missing email_body field' });
}
try {
const apiResult = await client.analyzeEmail(email_body, raw_headers);
const report = classifyEmail(apiResult, {
from: req.body.from || 'unknown',
subject: req.body.subject || 'unknown',
});
if (report.action === 'quarantine') {
quarantine.push({ ...report, timestamp: new Date().toISOString() });
} else if (report.action === 'review') {
reviewQueue.push({ ...report, timestamp: new Date().toISOString() });
}
return res.json({ success: true, ...report });
} catch (error) {
return res.status(500).json({ error: error.message });
}
});
// GET /quarantine - View quarantined emails
app.get('/quarantine', (req, res) => {
res.json({ count: quarantine.length, emails: quarantine });
});
// GET /review - View emails flagged for review
app.get('/review', (req, res) => {
res.json({ count: reviewQueue.length, emails: reviewQueue });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Email screening server running on port ${PORT}`);
});Test the screening service
Start the server and send test emails.
node server.jsTest with a JSON request (simplest approach):
# Screen an email body
curl -X POST http://localhost:3000/screen/json \
-H "Content-Type: application/json" \
-d '{
"from": "security@example-bank.com",
"subject": "Urgent: Account Verification Required",
"email_body": "Dear Customer,\n\nWe have detected unusual activity on your account. Please verify your identity immediately by clicking the link below:\n\nhttp://example-phishing-site.com/verify\n\nIf you do not verify within 24 hours, your account will be suspended.\n\nRegards,\nSecurity Team",
"raw_headers": "From: security@example-bank.com\nTo: user@company.com\nSubject: Urgent: Account Verification Required\nReceived-SPF: fail"
}'Test with a clean email:
curl -X POST http://localhost:3000/screen/json \
-H "Content-Type: application/json" \
-d '{
"from": "colleague@company.com",
"subject": "Meeting tomorrow",
"email_body": "Hi, just wanted to confirm our meeting tomorrow at 2 PM. Let me know if the time still works for you."
}'
# Check quarantine
curl http://localhost:3000/quarantineUnderstanding Header Analysis
If you do not include the raw_headers parameter, the API will skip SPF/DKIM/DMARC analysis. For the most accurate phishing detection, always include raw email headers.
The ScamVerify™ email analysis checks these authentication headers:
| Header Check | What It Detects |
|---|---|
| SPF (Sender Policy Framework) | Whether the sending server is authorized to send on behalf of the domain |
| DKIM (DomainKeys Identified Mail) | Whether the email content has been modified in transit |
| DMARC (Domain-based Message Authentication) | Whether SPF and DKIM align with the From domain |
| Return-Path mismatch | Whether the bounce address differs from the visible sender |
When any of these checks fail, the risk score is automatically boosted. SPF or DKIM failures add +15 to the risk score, and return path mismatches add +10. Multiple failures compound.
Complete Project Structure
email-screener/
.env # API key and port
scamverify.js # API client for email analysis
email-parser.js # Raw email parsing with mailparser
screener.js # Classification logic and quarantine thresholds
server.js # Express server with /screen and /quarantine endpoints
package.jsonNext Steps
- Email Analysis API Reference for full request and response schemas
- Insurance Claims Fraud Detection for screening claims correspondence
- E-commerce Fraud Prevention for support ticket screening