Skip to content

How to tell a business email from a personal one in code

· Updated · 6 min read · Business email

To tell a business email from a personal one, take the part after the @, reduce it to its registrable domain, and check it against a list of free webmail and consumer ISP domains. If it's on the list, it's personal. If it isn't, it's probably a company's own domain, and you can raise your confidence by checking the domain's MX records for consumer mail hosts. "Probably" matters: this is a classification with known failure cases, so your code should return a reason, not just a boolean.

Below is a working TypeScript implementation, followed by the cases where it breaks and what to do about each.

The check, step by step

  1. Parse the address. Split on the last @, lowercase the host, trim whitespace and any trailing dot. Don't try to fully validate the local part here.
  2. Reduce the host to its registrable domain. mail.stripe.com and stripe.com are the same organisation. Use a library backed by the Public Suffix List so example.co.uk comes out right. We explain why naive "last two labels" logic fails in eTLD+1 explained.
  3. Look the domain up in your lists. Free webmail (gmail.com, outlook.com, yahoo.com and their regional variants) and consumer ISP domains (comcast.net, btinternet.com) are personal. Disposable domains are personal and worth flagging. Which domains belong on the webmail list is less obvious than it looks, so we wrote a separate post on free email provider domains that actually matter. ISPs get their own treatment in why comcast.net is not a company.
  4. Handle institutional TLDs. .edu, .ac.uk, .gov, .gouv.fr and friends are neither personal nor a normal business. Give them their own label.
  5. For everything else, check MX. A custom domain whose mail is handled by a consumer provider (iCloud, mail.com, GMX) is almost always a person, not a company. A custom domain with no MX record can't receive mail at its own name and deserves a lower score.

A TypeScript implementation

This uses tldts for the Public Suffix List and Node's built-in DNS resolver. It type-checks under strict and runs on Node 18+; the free-provider list is shortened for the example.

import { getDomain } from 'tldts';
import { resolveMx } from 'node:dns/promises';

type Kind = 'business' | 'personal' | 'institution' | 'unknown' | 'invalid';
interface Result { kind: Kind; domain: string | null; reason: string }

const FREE_PROVIDERS = new Set([
  'gmail.com', 'googlemail.com', 'outlook.com', 'hotmail.com', 'live.com',
  'msn.com', 'yahoo.com', 'ymail.com', 'icloud.com', 'me.com', 'mac.com',
  'aol.com', 'proton.me', 'protonmail.com', 'gmx.com', 'gmx.de', 'web.de',
  'mail.com', 'yandex.ru', 'qq.com', '163.com', 'naver.com',
  // ...plus ISP domains such as comcast.net and btinternet.com
]);

// MX hosts that only serve consumer mailboxes.
const CONSUMER_MX = [
  /\.mail\.icloud\.com$/,             // iCloud, including iCloud+ custom domains
  /^mx\d+\.mail\.com$/,               // mail.com and its vanity domains
  /\.gmx\.net$/,                      // GMX
  /gmail-smtp-in\.l\.google\.com$/,   // consumer Gmail (Workspace uses other hosts)
];

const INSTITUTION = /\.(edu|gov|mil)$|\.(ac|edu|gov|gouv|gob)\.[a-z]{2}$/;

export async function classifyEmail(email: string): Promise<Result> {
  const at = email.lastIndexOf('@');
  if (at < 1) return { kind: 'invalid', domain: null, reason: 'no_at_sign' };

  const host = email.slice(at + 1).trim().toLowerCase().replace(/\.$/, '');
  const domain = getDomain(host);
  if (!domain) return { kind: 'invalid', domain: null, reason: 'not_a_registrable_domain' };

  if (FREE_PROVIDERS.has(domain)) return { kind: 'personal', domain, reason: 'free_provider' };
  if (INSTITUTION.test(host)) return { kind: 'institution', domain, reason: 'institutional_tld' };

  // Microsoft 365 tenant addresses: contoso.onmicrosoft.com is a company,
  // but onmicrosoft.com is not on the Public Suffix List, so keep the full host.
  if (host.endsWith('.onmicrosoft.com')) {
    return { kind: 'business', domain: host, reason: 'microsoft_365_tenant' };
  }

  let exchanges: string[];
  try {
    exchanges = (await resolveMx(host)).map((r) => r.exchange.toLowerCase().replace(/\.$/, ''));
  } catch {
    return { kind: 'unknown', domain, reason: 'no_mx_record' };
  }
  if (exchanges.some((h) => CONSUMER_MX.some((re) => re.test(h)))) {
    return { kind: 'personal', domain, reason: 'consumer_mail_host' };
  }
  return { kind: 'business', domain, reason: 'custom_domain' };
}

Some sample results:

Input kind reason
jane@gmail.com personal free_provider
jane@mail.stripe.com business custom_domain (domain stripe.com)
bob@engineer.com personal consumer_mail_host
it@contoso.onmicrosoft.com business microsoft_365_tenant
x@cs.harvard.edu institution institutional_tld

Two implementation notes. The MX lookup runs against the full host, because that's where mail for the address actually goes. And the catch treats every DNS error as "no MX". In production, distinguish ENOTFOUND and ENODATA (the domain has no mail setup) from ETIMEOUT (your resolver had a bad moment), and fail open on the latter. Mail servers can fall back to a domain's A record when there's no MX, so "no MX" is a reason to look harder, not proof of a fake.

Where "custom domain means business" breaks

The list lookup handles the bulk of real traffic. The interesting work is in the cases that look like a business and aren't, or the reverse.

Personal custom domains

Consumers can put their own domain on a consumer mailbox. Apple's iCloud+ supports custom email domains, and its setup guide has you point the domain's MX at mx01.mail.icloud.com and mx02.mail.icloud.com. That's why the MX check above catches jane@smithfamily.net hosted on iCloud. Microsoft 365 Personal and Family subscribers can also connect a domain (currently only ones managed by GoDaddy) to an Outlook.com mailbox, according to Microsoft's domains FAQ. We haven't found a reliable MX pattern that distinguishes those from business Microsoft 365 tenants, so they'll come out as business. Accept that miss.

Vanity domains on webmail providers

mail.com lets free users pick from a long catalogue of domains that read like professions or places. engineer.com, consultant.com, usa.com and myself.com all have MX records pointing at mx00.mail.com and mx01.mail.com (we checked on 12 October 2026). None of them is on most free-provider lists, and jane@consultant.com looks very much like a business address. The MX rule catches them without you maintaining the full catalogue.

Microsoft 365 tenant domains

Every Microsoft 365 organisation gets a something.onmicrosoft.com domain at signup, and Microsoft's FAQ notes it "still works for email" after a custom domain is added. Because onmicrosoft.com is not on the Public Suffix List, a PSL-based library collapses every tenant to onmicrosoft.com. If you then enrich that domain, you get Microsoft's company profile for every tenant. The special case in the code keeps the full host instead.

Google and Microsoft MX don't mean personal

Don't classify by "is the MX Google?" Companies on Google Workspace route mail through hosts like aspmx.l.google.com (that's what stripe.com uses), while consumer Gmail uses gmail-smtp-in.l.google.com. The same goes for Microsoft: business tenants typically use a host ending in mail.protection.outlook.com. Only match hosts that serve consumer mailboxes exclusively.

Real businesses on personal addresses

Plenty of founders, freelancers and small shops run their business from a Gmail account. Your classifier will correctly say "personal domain", and that can still be a paying B2B customer. Whether to let them in is a product decision, covered in should your B2B signup form require a work email.

Signals that look useful and aren't

  • Whether the domain has a website. Many legitimate small companies have a mail-only domain, and plenty of personal domains have a homepage.
  • The local part. firstname.lastname@ is common at companies and on Gmail alike. Role addresses like info@ say something, but not "business vs personal".
  • Domain age or TLD. A .io domain registered last month can be a real startup. These are fraud signals, not classification signals.

Doing it with an API instead

If you'd rather not maintain the lists, pingutil does steps 1 to 4 server-side: it lowercases, punycodes IDNs, reduces to the registrable domain, and returns a category of free, disposable, isp, educational, government, company or unknown. Set check_dns: true to get domain_signals.can_receive_email.

const res = await fetch('https://pingutil.com/v1/lookup', {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.PINGUTIL_KEY}`,
    'content-type': 'application/json',
  },
  body: JSON.stringify({ input: email, skip_enrichment: true, check_dns: true }),
});
const data = await res.json();

const kind =
  data.category === 'company' ? 'business'
  : ['free', 'isp', 'disposable'].includes(data.category) ? 'personal'
  : ['educational', 'government'].includes(data.category) ? 'institution'
  : 'invalid';

const canReceive = data.domain_signals?.can_receive_email ?? null;

Two honest limits. pingutil does not apply the consumer-MX rule, so a mail.com vanity domain or an iCloud+ custom domain comes back as company. And it also keys on the registrable domain, so handle .onmicrosoft.com hosts before you call it. For company results, drop skip_enrichment to get the company profile as well; finding the company behind an email address covers what to do with it. Field details are in the API reference.

The short version

  • Registrable domain on a webmail, ISP or disposable list: personal.
  • Institutional TLD: its own bucket.
  • Custom domain with consumer MX hosts: personal.
  • Custom domain with business MX hosts: business.
  • No MX, DNS errors or odd hosts: unknown, and decide your policy for unknown explicitly.

Questions people ask

Is a Google Workspace address a business email?
Usually, yes. Workspace runs on the customer's own domain, so the address looks like jane@acme.com and its MX records point at Google's business mail hosts rather than the consumer Gmail ones. Classify it by its domain like any other custom domain.
Can I tell business from personal email with a regex?
No. A regex can check the shape of an address, but whether a domain belongs to a company or a webmail provider is a data question. You need a provider list, and ideally a DNS lookup, to answer it.
Is a .edu or .gov address a business email?
Treat them as their own categories. They belong to institutions, not consumer webmail, but most B2B products route them differently from company leads, for example to education pricing or a public sector team.