diff --git a/apps/landing/src/pages/tools/email-headers.tsx b/apps/landing/src/pages/tools/email-headers.tsx new file mode 100644 index 0000000..227f30c --- /dev/null +++ b/apps/landing/src/pages/tools/email-headers.tsx @@ -0,0 +1,655 @@ +import {FAQSection, Footer, Navbar, SectionHeader} from '../../components'; +import {motion} from 'framer-motion'; +import {DASHBOARD_URI} from '../../lib/constants'; +import React, {useState} from 'react'; +import {NextSeo} from 'next-seo'; +import {AlertTriangle, ArrowRight, CheckCircle, FileText, XCircle} from 'lucide-react'; +import {Button} from '@plunk/ui'; +import {Bricolage_Grotesque, Hanken_Grotesk, JetBrains_Mono} from 'next/font/google'; +import Link from 'next/link'; +import type {FAQ} from '../../components/FAQSection'; + +const display = Bricolage_Grotesque({ + subsets: ['latin'], + variable: '--font-display', + display: 'swap', + weight: ['400', '500', '600', '700', '800'], +}); + +const body = Hanken_Grotesk({ + subsets: ['latin'], + variable: '--font-body', + display: 'swap', + weight: ['400', '500', '600', '700'], +}); + +const mono = JetBrains_Mono({ + subsets: ['latin'], + variable: '--font-mono', + display: 'swap', + weight: ['400', '500'], +}); + +interface AuthResult { + mechanism: 'spf' | 'dkim' | 'dmarc'; + result: string; + detail: string; + pass: boolean | null; +} + +interface ReceivedHop { + from: string; + by: string; + timestamp: string | null; + raw: string; +} + +interface ParsedHeaders { + from: string | null; + to: string | null; + subject: string | null; + date: string | null; + messageId: string | null; + replyTo: string | null; + returnPath: string | null; + xMailer: string | null; + dkimSelector: string | null; + dkimDomain: string | null; + authResults: AuthResult[]; + hops: ReceivedHop[]; +} + +function parseRawHeaders(raw: string): Record { + const headers: Record = {}; + const lines = raw.split(/\r?\n/); + let currentKey = ''; + let currentValue = ''; + + for (const line of lines) { + if (/^\s/.test(line) && currentKey) { + currentValue += ' ' + line.trim(); + } else { + if (currentKey) { + const existing = headers[currentKey]; + if (existing) existing.push(currentValue.trim()); + else headers[currentKey] = [currentValue.trim()]; + } + const colonIdx = line.indexOf(':'); + if (colonIdx > 0) { + currentKey = line.slice(0, colonIdx).trim().toLowerCase(); + currentValue = line.slice(colonIdx + 1).trim(); + } else { + currentKey = ''; + currentValue = ''; + } + } + } + if (currentKey) { + const existing = headers[currentKey]; + if (existing) existing.push(currentValue.trim()); + else headers[currentKey] = [currentValue.trim()]; + } + return headers; +} + +function parseAuthResults(authHeader: string): AuthResult[] { + const results: AuthResult[] = []; + const lower = authHeader.toLowerCase(); + + const mechanisms: Array<'spf' | 'dkim' | 'dmarc'> = ['spf', 'dkim', 'dmarc']; + for (const mech of mechanisms) { + const mechIdx = lower.indexOf(mech + '='); + if (mechIdx === -1) continue; + + const afterMech = authHeader.slice(mechIdx + mech.length + 1); + const resultMatch = /^(\w+)/.exec(afterMech); + const result = resultMatch?.[1]?.toLowerCase() ?? 'unknown'; + + const pass = + result === 'pass' + ? true + : result === 'fail' || result === 'hardfail' || result === 'none' + ? false + : null; + + let detail = ''; + if (mech === 'spf') { + const smtpMatch = /smtp\.mailfrom\s*=\s*([^\s;]+)/i.exec(authHeader.slice(mechIdx)); + if (smtpMatch?.[1]) detail = `smtp.mailfrom=${smtpMatch[1]}`; + } else if (mech === 'dkim') { + const headerMatch = /header\.i\s*=\s*([^\s;]+)/i.exec(authHeader.slice(mechIdx)); + if (headerMatch?.[1]) detail = `header.i=${headerMatch[1]}`; + } else if (mech === 'dmarc') { + const headerMatch = /header\.from\s*=\s*([^\s;]+)/i.exec(authHeader.slice(mechIdx)); + if (headerMatch?.[1]) detail = `header.from=${headerMatch[1]}`; + } + + results.push({mechanism: mech, result, detail, pass}); + } + + return results; +} + +function parseReceivedHop(receivedValue: string): ReceivedHop { + const fromMatch = /from\s+([^\s]+)/i.exec(receivedValue); + const byMatch = /by\s+([^\s]+)/i.exec(receivedValue); + + const datePattern = /;\s*(.+)$/; + const dateMatch = datePattern.exec(receivedValue); + let timestamp: string | null = null; + if (dateMatch?.[1]) { + const parsed = new Date(dateMatch[1].trim()); + if (!isNaN(parsed.getTime())) { + timestamp = parsed.toLocaleString('en-US', { + month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', + hour12: false, + }); + } + } + + return { + from: fromMatch?.[1] ?? '(unknown)', + by: byMatch?.[1] ?? '(unknown)', + timestamp, + raw: receivedValue, + }; +} + +function parseDkimSignature(dkimValue: string): {selector: string | null; domain: string | null} { + const sMatch = /\bs\s*=\s*([^;\s]+)/i.exec(dkimValue); + const dMatch = /\bd\s*=\s*([^;\s]+)/i.exec(dkimValue); + return {selector: sMatch?.[1] ?? null, domain: dMatch?.[1] ?? null}; +} + +function analyzeHeaders(raw: string): ParsedHeaders | null { + if (!raw.trim()) return null; + + const headers = parseRawHeaders(raw); + const first = (key: string) => headers[key]?.[0] ?? null; + + const authHeader = first('authentication-results'); + const authResults = authHeader ? parseAuthResults(authHeader) : []; + + const receivedHeaders = headers['received'] ?? []; + const hops = receivedHeaders.map(parseReceivedHop).reverse(); + + const dkimSig = first('dkim-signature'); + const {selector, domain} = dkimSig ? parseDkimSignature(dkimSig) : {selector: null, domain: null}; + + return { + from: first('from'), + to: first('to'), + subject: first('subject'), + date: first('date'), + messageId: first('message-id'), + replyTo: first('reply-to'), + returnPath: first('return-path'), + xMailer: first('x-mailer') ?? first('x-mailing-list') ?? null, + dkimSelector: selector, + dkimDomain: domain, + authResults, + hops, + }; +} + +function AuthBadge({result}: {result: string}) { + const pass = result === 'pass'; + const fail = result === 'fail' || result === 'hardfail'; + const cls = pass + ? 'bg-green-100 text-green-700' + : fail + ? 'bg-red-100 text-red-700' + : 'bg-neutral-100 text-neutral-600'; + return ( + + {result} + + ); +} + +const faqs: FAQ[] = [ + { + question: 'Where do I find the raw email headers?', + answer: 'In Gmail: open the email, click the three-dot menu (⋮), choose "Show original". In Outlook: open the email, click File → Properties, and copy the "Internet headers" box. In Apple Mail: with the email open, go to View → Message → All Headers. The raw headers appear at the top of the message source.', + }, + { + question: 'What do the Authentication-Results headers mean?', + answer: 'Authentication-Results is added by the receiving mail server and summarises the SPF, DKIM, and DMARC check results. "pass" means the check succeeded. "fail" or "hardfail" means it failed. "none" means no record was found. If DMARC passes but SPF or DKIM fails, check that your sending domain aligns with the From header.', + }, + { + question: 'How do I read the Received chain?', + answer: 'Received headers are added by each mail server the message passed through, with the most recent at the top. Reading them bottom-to-top gives you the routing path from sender to inbox. Large time gaps between hops indicate server delays or queuing. The very first Received header shows where the message originated.', + }, + { + question: 'Why does email land in spam even though SPF and DKIM pass?', + answer: 'Authentication passing is necessary but not sufficient. Spam filters also consider sender reputation, IP blacklist status, content quality, engagement history, and DMARC alignment. A brand-new IP, a domain with no sending history, or content with many spam trigger words can cause filtering even with perfect authentication.', + }, + { + question: 'What is the DKIM-Signature header?', + answer: 'DKIM-Signature contains a cryptographic signature applied by the sending server. The "d=" tag identifies the signing domain and "s=" is the selector used to locate the public key in DNS. Receiving servers verify the signature against the public key at ._domainkey.. A mismatch or missing key causes a DKIM fail.', + }, +]; + +export default function EmailHeadersPage() { + const [raw, setRaw] = useState(''); + const [result, setResult] = useState(null); + const [analysed, setAnalysed] = useState(false); + + const handleAnalyze = (e: React.FormEvent) => { + e.preventDefault(); + const parsed = analyzeHeaders(raw); + setResult(parsed); + setAnalysed(true); + }; + + const handleClear = () => { + setRaw(''); + setResult(null); + setAnalysed(false); + }; + + const metaFields: Array<{label: string; value: string | null}> = result + ? [ + {label: 'From', value: result.from}, + {label: 'To', value: result.to}, + {label: 'Subject', value: result.subject}, + {label: 'Date', value: result.date}, + {label: 'Message-ID', value: result.messageId}, + {label: 'Reply-To', value: result.replyTo}, + {label: 'Return-Path', value: result.returnPath}, + {label: 'X-Mailer', value: result.xMailer}, + ].filter(f => f.value !== null) + : []; + + return ( + <> + + + + +
+
+ {/* ========== HERO ========== */} +
+
+ +
+ + § T-09  —  Tool + + ← All tools + + + + +

+ Email headers +
+ analyzer +

+

+ Paste raw email headers and get a parsed breakdown of SPF, DKIM, and DMARC results, routing hops with timestamps, and the full authentication chain. +

+
+
+
+ + {/* ========== TOOL ========== */} +
+ +
+
+
+ + + Raw header input + +
+
+ +
+
+
+ +