Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

Guide

How to Detect Mail Server Patterns Before Verifying Emails (DNS, SMTP and LeadOcean)

Six server patterns make a plain 250 reply mean different things. Detect each with dig and one SMTP probe, then size your list by email_status. No records needed.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

Email pattern detection for verification means working out how a domain's mail server answers probes, before you trust any single reply. This guide shows the DNS lookup, the SMTP probe and the classifier, then sizes a real segment by status in LeadOcean.

Do not confuse this with the address pattern, such as first.last@acme.com. That one is covered in what is an email pattern. Here a pattern is a server behaviour.

Related: email verification explained, best email verification tools, the deliverability and verification guide and pricing. The hub for this section is the LeadOcean blog.

TL;DR

  • A 250 reply to RCPT TO is not proof of a mailbox. Check MX records, probe a real and a random address, and time both.
  • Six patterns cover almost every result: standard, catch-all, greylisting, tarpit, security gateway and no mail server.
  • The walkthrough takes about 40 minutes and spends 0 records. Sizing by emailStatus is free on every plan, Free included.

Prerequisites

  • A LeadOcean API key from app.leadocean.io, exported as $LEADOCEAN_API_KEY. The free plan needs no card.
  • dig, nc and Python 3.
  • A machine that can open outbound port 25. Most cloud hosts and home networks block it. If yours does, steps 2 and 3 will not run there.
  • Permission to probe the domains you test. Use your own domains or ones you have a reason to check.

Step 1: Read the MX records

The MX lookup settles two of the six patterns before you open a connection. No MX means no mail server. A gateway hostname in the MX list means a security gateway sits in front of the mailbox.

bash
dig +short MX acme.com

A domain with no usable MX can still publish a null MX. RFC 7505 defines 0 . as an explicit statement that the domain accepts no mail (checked September 2026). Treat it as invalid and stop.

Gateways show up in the hostnames. These suffixes are common ones.

MX hostname ends inGateway
pphosted.comProofpoint
mimecast.comMimecast
barracudanetworks.comBarracuda
iphmx.comCisco

Gateways accept at SMTP and filter afterwards. A 250 from one tells you the gateway is happy, not that the mailbox exists. The LeadMagic guide names the same gateway family (checked September 2026).

Step 2: Probe a real address and a random one

Open an SMTP session to the lowest-preference MX and ask about two recipients. You stop before DATA, so no message is sent. The RFC 5321 reply codes are the signal: 2yz is accept, 4yz is temporary, 5yz is permanent.

bash
nc mx1.acme.com 25
EHLO probe.example.net
MAIL FROM:<probe@example.net>
RCPT TO:<jane.doe@acme.com>
RCPT TO:<qzkxwvhtnbpmrd@acme.com>
QUIT

Read the two RCPT replies side by side. A real 250 and a random 550 is a standard server that tells the truth. A 250 for both is a catch-all.

A 450 or 451 on the first address is a deferral. RFC 5321 section 4.5.4.1 says a sender should retry a temporary failure, with a gap of at least 30 minutes between tries (checked September 2026). That is the greylisting pattern: the server refuses a first-time sender and accepts the retry.

Time the session as well. RFC 5321 section 4.5.3.2 allows a server five minutes to answer RCPT TO, so a server can hold you for a long time and stay within the rules. A tarpit uses that room to slow automated clients down.

Step 3: Classify with a script

Run the probe in code so the decision rules are written down. This script uses only the standard library plus dig. It checks MX, flags gateways, probes a real and a random address, and times both.

python
import random, smtplib, string, subprocess, sys, time

GATEWAYS = {"pphosted.com": "Proofpoint", "mimecast.com": "Mimecast",
            "barracudanetworks.com": "Barracuda", "iphmx.com": "Cisco"}
SLOW = 10  # seconds. A starting point, not a standard.

def mx_hosts(domain):
    out = subprocess.run(["dig", "+short", "MX", domain],
                         capture_output=True, text=True).stdout
    rows = [line.split() for line in out.splitlines() if line.strip()]
    rows = sorted((int(pref), host.rstrip(".").lower()) for pref, host in rows)
    return [host for _, host in rows if host]  # a null MX ("0 .") leaves nothing

def rcpt(host, address, timeout=30):
    start = time.monotonic()
    try:
        s = smtplib.SMTP(host, 25, timeout=timeout)
        try:
            s.ehlo("probe.example.net")
            s.mail("probe@example.net")
            code, _ = s.rcpt(address)
        finally:
            s.close()
    except (smtplib.SMTPException, OSError):
        code = None
    return code, round(time.monotonic() - start, 1)

def classify(address):
    domain = address.split("@")[1]
    hosts = mx_hosts(domain)
    if not hosts:
        return {"domain": domain, "pattern": "no_mail_server"}
    gateway = next((name for h in hosts for sfx, name in GATEWAYS.items()
                    if h.endswith(sfx)), None)
    fake = "".join(random.choices(string.ascii_lowercase, k=14)) + "@" + domain
    real, t_real = rcpt(hosts[0], address)
    junk, t_junk = rcpt(hosts[0], fake)
    if real is None:
        pattern = "unreachable"
    elif 400 <= real < 500:
        pattern = "greylisted_or_deferred"
    elif max(t_real, t_junk) > SLOW:
        pattern = "tarpit"
    elif real == 250 and junk == 250:
        pattern = "catch_all"
    elif real == 250:
        pattern = "standard_accepts"
    else:
        pattern = "standard_rejects"
    return {"domain": domain, "mx": hosts[0], "gateway": gateway,
            "rcpt_real": real, "rcpt_random": junk,
            "seconds": max(t_real, t_junk), "pattern": pattern}

if __name__ == "__main__":
    for addr in sys.argv[1:]:
        print(classify(addr))

Run it as python3 probe.py jane.doe@acme.com. Keep the volume low and the rate slow. Hitting one server with thousands of probes from one IP gets that IP blocked.

The gateway flag is separate from the pattern. A domain can be a catch-all behind Proofpoint, and you want to know both.

This is also where the do-it-yourself route stops paying. Port 25 access, IP reputation and retry queues are a project of their own. If you only need to know which rows to send to, read the status your data provider already holds, as in the next step.

Step 4: Size the same segment by LeadOcean status

LeadOcean stores a deliverability status on each person's best email. The field is email_status, the filter is emailStatus, and there are 13 values. None of them names a gateway, which is why step 1 stays useful.

A sizing call with count=true and limit=1 is free. Run it once per status for the same segment.

bash
curl -X POST "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified"],"emailType":["work"],"limit":1}'

Repeat with ["catch_all"], ["catch_all_valid"] and ["unknown","risky"]. Read meta.total each time. It is capped at 100,000.

Three of those statuses map straight onto patterns:

  • catch_all: the domain accepts every address, so the mailbox is unconfirmed.
  • catch_all_valid: a catch-all domain where the mailbox was confirmed by the provider's identity check. That covers Microsoft 365 managed tenants and Google Workspace only.
  • unknown: a check ran and was inconclusive. Deferrals and slow servers belong in this bucket.

The search index carries no per-address verification record. A search count of verified is an upper bound, and enrichment gives the final answer for one person.

Step 5: Route each pattern

Give every pattern one action, and write the actions down before the first send.

PatternSignalAction
Standard, accepts250 real, 550 randomSend.
Standard, rejects550 on the real addressSuppress.
Catch-all250 for bothSend a small monitored batch, or keep to catch_all_valid.
Greylisting450 or 451Retry after 30 minutes or more, then decide.
TarpitReplies slower than your thresholdAllow for the delay once, then treat as unknown.
GatewayMX on a gateway suffixSend from a warmed domain and watch late bounces.
No mail serverNo MX or null MXSuppress.

LeadOcean does not refund or credit back bounced emails. Read email_status before you send, not after.

To pull only the rows you chose, export with the same filters. Bulk CSV export is not on the Free plan. It is included from Solo ($97 a month) up. The app's Exports page has the same filter builder and shows the record price first.

bash
curl -X POST https://api.leadocean.io/v1/exports \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"vp-sales-ca-verified","filters":{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified","catch_all_valid"],"emailType":["work"]},"limit":25,"columns":["person_id","profile_full_name","current_company_domain","email_N_address","email_N_status"]}'

The N columns come back as email_1_address and email_1_status. One export takes up to 50,000 rows, and an account takes 500,000 a day. This test costs 25 records.

What you get

First, a real MX lookup for the reserved domain example.com, run on 2026-10-21. The answer is a null MX, the no-mail-server pattern.

text
$ dig MX example.com
;; ANSWER SECTION:
example.com.		300	IN	MX	0 .

Second, the classifier output for a placeholder domain. This is a sample of the output shape with placeholder values, not a live run.

text
{'domain': 'acme.com', 'mx': 'mx1.acme-example.com', 'gateway': None,
 'rcpt_real': 250, 'rcpt_random': 250, 'seconds': 0.6, 'pattern': 'catch_all'}

Third, the free count_leads MCP tool, run on 2026-10-21. The segment is VPs in Sales & Business Development in Canada with a work email. Each row is a count by one emailStatus value. None of them is a mailable total.

emailStatus filterTotalExact
verified1,602yes
catch_all (also matches catch_all_valid)2,993yes
catch_all_valid0yes
unknown or risky1,161yes

In this segment the catch_all slice is nearly twice the verified slice, and none of it is a confirmed catch_all_valid mailbox. A plain export of both would put a large unconfirmed block in your sequence. That is the case for detecting patterns before you send.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerCheck $LEADOCEAN_API_KEY is exported and the header is lowercase x-api-key.
402Out of records. On the free plan the 1,000 are a one-off allowanceSize with count=true before pulling rows. Move to Solo ($97 a month) or Agency ($497 a month).
429Above your plan's rate limit per account (5 a second on Free, 200 on Agency)Back off and honour the Retry-After header.
Empty data on a filter that should matchA bad enum value, such as vp where the list wants VPLook up the exact string with GET /v1/enums/{name} or the free list_enum_values MCP tool.
meta.total stops at 100,000The count is capped, so totalIsExact is falseTreat it as a floor and narrow the filters.
400 export_limit_rows or 429 export_limit_dailyOver 50,000 rows in one export, or 500,000 in a daySplit the export by country or seniority. The daily limit resets, so run the rest later.

FAQ

Can I tell a catch-all from a standard server with one probe?

No. One probe returns one reply. You need a real address and a random one at the same domain, and you compare the two replies.

Is a greylisted address invalid?

No. A 450 or 451 is a temporary refusal. Retry after at least 30 minutes before you decide, and mark it unknown until then.

Does LeadOcean tell me which domains sit behind a gateway?

No. The 13 email_status values do not name a gateway. Check the MX hostnames from step 1, and read email_status for the mailbox itself.

Do I need to run SMTP probes myself?

Usually not. Probing needs port 25, a clean IP and slow, patient retries. For most teams it is cheaper to filter on emailStatus and read the status that comes with each record.

How many records does this cost?

Steps 1 to 4 spend none. Sizing is free. The optional 25-row export in step 5 spends 25 records and needs Solo or above.

Know which addresses are safe before you send

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →