DEV Community

Michael Doyle
Michael Doyle

Posted on

I built an email scorer in one HTML file with zero dependencies. Here is every threshold and where it came from.

I built an email scorer in one HTML file with zero dependencies. Here is every threshold and where it came from.

I write about B2B sales for a living, which means I read a lot of cold emails and a lot of advice about cold emails. The advice is almost always unfalsifiable. "Keep it short." How short? "Be personal." Measured how?

So I turned the advice into numbers and put the numbers in a file. One index.html, about 220 lines, no framework, no build step, no API call. You paste a subject line and a body, it returns a score out of 100 across eight checks.

Live version: Proposal Email Scorer. Source: github.com/leaderr-dev/proposal-email-scorer, MIT.

The interesting part is not the UI, it is deciding what is measurable. Here is the whole thing.

The constraint: everything deterministic

The obvious way to build this in 2026 is to send the email to a model and ask for a score. I did not want that, for three reasons.

The first is that the same input has to produce the same output. If you paste an email, tweak one word and paste it again, a two point move should mean something. A model gives you a different number on the same input and you learn nothing.

The second is privacy. People paste real emails about real deals into a tool like this. If there is no network call, there is nothing to explain.

The third is that it made me define the thresholds instead of hiding behind a model. Every number below is a decision I had to justify.

Subject length, 28 to 55 characters

Characters, not words. Mobile clients truncate somewhere around 40 to 55 depending on the device, so 55 is the ceiling. The 28 floor is the softer claim: short subjects correlate with automation because automated subjects are short.

if (subj.length >= 28 && subj.length <= 55) { /* 15 points */ }
else if (subj.length < 28) { /* 8 points */ }
else { /* 6 points */ }
Enter fullscreen mode Exit fullscreen mode

Over-length scores lower than under-length, because a truncated subject actively hides information.

All caps and exclamation marks

var caps  = (subj.match(/\b[A-Z]{3,}\b/g) || []).length;
var bangs = (subj.match(/[!]/g) || []).length;
Enter fullscreen mode Exit fullscreen mode

The {3,} matters. Without it, "SDR", "CRM", "B2B" and every other sales acronym flags. Three-plus-letter runs still catch acronyms, which is a known false positive I decided to live with, because a subject line stuffed with acronyms is its own problem.

Spam phrases

A 67 entry list, matched as substrings against subject and body joined together:

var low = " " + t.toLowerCase() + " ";
for (var i = 0; i < SPAM.length; i++) {
  if (low.indexOf(SPAM[i]) > -1) hits.push(SPAM[i]);
}
Enter fullscreen mode Exit fullscreen mode

Scoring is banded rather than linear: zero hits is full marks, one or two is half, three or more is zero. That reflects how filters actually behave. One flagged phrase in an otherwise normal message is noise. Four is a pattern.

The uncomfortable finding while assembling the list is how much of it is ordinary polite sales writing. "No obligation." "Risk free." "Limited time." These are not things spammers say and salespeople avoid. They are things salespeople say constantly.

Body length, 50 to 150 words

Under 50 there is not enough to say yes to. Over 150 reply rates fall and keep falling. Over 250 scores near zero.

Reading grade

Standard Flesch Kincaid:

0.39 * (words / sentences) + 11.8 * (syllables / words) - 15.59
Enter fullscreen mode Exit fullscreen mode

Syllable counting is where this gets approximate. The heuristic:

function syl(w) {
  w = w.toLowerCase().replace(/[^a-z]/g, "");
  if (!w) return 0;
  if (w.length <= 3) return 1;
  w = w.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, "").replace(/^y/, "");
  var m = w.match(/[aeiouy]{1,2}/g);
  return m ? m.length : 1;
}
Enter fullscreen mode Exit fullscreen mode

It strips silent trailing e, es and ed, then counts vowel groups. It gets "business" and "meeting" right and "queue" wrong. For a whole email the errors wash out, which is why the tool reports a grade to one decimal and not a certified score. Target is 8 or below.

Call to action

This is the check I am least happy with, because it is keyword matching:

var CTA = ["are you open", "worth a", "would you be open",
           "do you have time", "15 minutes", "quick call", ...];
Enter fullscreen mode Exit fullscreen mode

Plus a fallback: if the body ends in a question mark, that counts. It catches most real asks and it will miss an unusually phrased one. The alternative was a model call, which breaks the determinism rule. Keyword matching with a documented blind spot beat a black box with none.

Personalisation

Escape the company name, count occurrences:

new RegExp(co.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")
Enter fullscreen mode Exit fullscreen mode

The escape is not optional. Real company names contain dots and plus signs, and an unescaped . matches any character, so "A.B." would match "Abx".

This check only tells you the name is present. It cannot tell you whether what surrounds it is specific or a mail merge compliment. That is the limit of a rule based tool, and it is the check that matters most, which is the honest tension in the whole project.

You to us ratio

Count you|your|yours against i|we|our|us|my. Second person should at least match first person. Five points, the lowest weight, because it is a diagnostic rather than a rule. A bad ratio means the email leads with what your company does, and fixing it means rewriting the argument rather than swapping pronouns.

Weights

Spam phrases 20, subject length 15, body length 15, CTA 15, subject shouting 10, reading grade 10, personalisation 10, ratio 5. Above 80 send, 60 to 79 fix something first, below 60 do not send.

What it deliberately does not do

It does not check DNS records, warm-up state or sending reputation, which matter more than any of the above and cannot be checked from a pasted string. It does not judge whether your offer is any good. And it will not write the email.

For that last part I use the leaderr.io proposal generator, which takes your site and the prospect's site and drafts the email, and the leaderr.io dossier generator for the company research that makes check seven mean something. Full disclosure, I write for Leaderr, which is also why I had a stack of proposal emails to test this against.

The repo is MIT. If a threshold looks wrong, the numbers are all in one file and I would rather be argued out of one than keep defending it.

Top comments (0)