<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kadir</title>
    <description>The latest articles on DEV Community by Kadir (@kadirify).</description>
    <link>https://dev.to/kadirify</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4196981%2F5134c16f-83b0-4b2f-af6a-c03ec9d45d40.png</url>
      <title>DEV Community: Kadir</title>
      <link>https://dev.to/kadirify</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kadirify"/>
    <language>en</language>
    <item>
      <title>I built an open-source phishing email analyzer that never sends your email anywhere</title>
      <dc:creator>Kadir</dc:creator>
      <pubDate>Tue, 20 Oct 2026 22:50:41 +0000</pubDate>
      <link>https://dev.to/kadirify/i-built-an-open-source-phishing-email-analyzer-that-never-sends-your-email-anywhere-h88</link>
      <guid>https://dev.to/kadirify/i-built-an-open-source-phishing-email-analyzer-that-never-sends-your-email-anywhere-h88</guid>
      <description>&lt;p&gt;Most phishing advice stops at "be careful." That's not very useful when an&lt;br&gt;
email looks pixel-perfect identical to your bank's. I wanted something that&lt;br&gt;
points at the actual line and says: &lt;em&gt;this&lt;/em&gt; is the problem, and here's why.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Kanca&lt;/strong&gt; — an open-source tool that reads the raw source of an&lt;br&gt;
email the way a security person would: who really sent it, which servers it&lt;br&gt;
passed through, where the links actually go, what's in the attachments. It&lt;br&gt;
marks every suspicious part directly on the source and explains why it&lt;br&gt;
matters.&lt;/p&gt;

&lt;p&gt;🔗 Live: &lt;a href="https://kanca.kadirify.com/" rel="noopener noreferrer"&gt;https://kanca.kadirify.com/&lt;/a&gt;&lt;br&gt;
🔗 Repo: &lt;a href="https://github.com/Kadirify/kanca" rel="noopener noreferrer"&gt;https://github.com/Kadirify/kanca&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The two constraints that shaped everything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No AI.&lt;/strong&gt; The score comes from 50 rules anyone can read in the repo. Same&lt;br&gt;
email in, same result out, every time. No model drift, no "the API changed&lt;br&gt;
its mind," no black box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing leaves the browser.&lt;/strong&gt; The email you paste is parsed, matched&lt;br&gt;
against rules, and scored entirely client-side. The one optional exception&lt;br&gt;
is a DNS lookup (SPF/DMARC/MX) on the sender's domain — and even then, only&lt;br&gt;
the bare domain goes out, never anything from the email itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the same code run in three places
&lt;/h2&gt;

&lt;p&gt;I wanted the exact same analysis logic to run in the browser, in a Node CLI&lt;br&gt;
(for testing against sample emails), and in the test suite — no duplicated&lt;br&gt;
logic, no "works in browser but not in tests."&lt;/p&gt;

&lt;p&gt;The trick: isolate the two environment-specific pieces — HTML parsing and&lt;br&gt;
DNS resolution — behind interfaces, and inject the real implementation at&lt;br&gt;
the entry point.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;&lt;br&gt;
@kanca/engine/browser  →  DOMParser + fetch-based DNS resolver&lt;br&gt;
@kanca/engine/node     →  cheerio + node:dns&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Everything else — MIME parsing, the 50 rules, brand-lookalike matching with&lt;br&gt;
Levenshtein distance, the scoring logic — is plain TypeScript with zero&lt;br&gt;
environment assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoring, briefly
&lt;/h2&gt;

&lt;p&gt;Each triggered rule adds its weight. Repeats of the same rule only add 25%&lt;br&gt;
of the weight each time (so one spammy pattern repeated 10 times doesn't&lt;br&gt;
dominate the score). Each layer (headers, DNS, domain, links, attachments,&lt;br&gt;
content) has a cap, so no single weak signal can push a score to "high risk"&lt;br&gt;
on its own. +5 bonus if three or more layers have findings. Capped at 100.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;pnpm monorepo, TypeScript, ESM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsm8yf1qc4pe9mzxlmhp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsm8yf1qc4pe9mzxlmhp.jpg" alt=" " width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzf73a5mn3fzzw9ib7zlb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzf73a5mn3fzzw9ib7zlb.jpg" alt=" " width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0dofui873g9jxutwgva.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0dofui873g9jxutwgva.jpg" alt=" " width="750" height="1624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;postal-mime for MIME parsing, tldts for domain parsing&lt;/li&gt;
&lt;li&gt;React 19 + Vite 6 on the frontend&lt;/li&gt;
&lt;li&gt;Hono for the one API route (DNS lookups only)&lt;/li&gt;
&lt;li&gt;Vitest for tests&lt;/li&gt;
&lt;li&gt;Deployed on Vercel&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://kanca.kadirify.com/" rel="noopener noreferrer"&gt;https://kanca.kadirify.com/&lt;/a&gt; — paste a suspicious email (or try one of the&lt;br&gt;
built-in samples) and see what it finds. English and Turkish UI.&lt;/p&gt;

&lt;p&gt;Open to feedback, especially on rules that feel off or phishing patterns&lt;br&gt;
I'm missing. It's MIT licensed and actively maintained.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>security</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
