Email intelligence in one call.
mailintel takes an email address or a domain, resolves the provider from public DNS, and returns the exact IMAP/SMTP connection settings. Zero dependencies, fully typed, and it runs anywhere fetch exists.
Quickstart
Pass an email address and the username is resolved for you. Pass a bare domain and you get the same connection settings with a username template instead.
import { mailintel } from "mailintel";
const result = await mailintel("[email protected]");
if (result.connection) {
const { imap, smtp, username } = result.connection;
// imap -> { host: "imap.gmail.com", port: 993, security: "tls" }
// smtp -> { host: "smtp.gmail.com", port: 465, security: "tls" }
// username -> "[email protected]"
}It also accepts an array and returns results in the same order:
const results = await mailintel([
"[email protected]",
"example.com",
"[email protected]",
]);
// results[1].connection.username === "%EMAILADDRESS%"
// (no email was supplied, so the template is not resolved)How it works
Every mail domain publishes MX records that say which servers accept its email. mailintel reads those over DNS-over-HTTPS, matches the host against known provider patterns, and maps the provider to IMAP/SMTP settings.
input
└── cache
├── hit → return cached result
└── miss
├── domain override? → high confidence, source: override
├── known domain? → high confidence, source: known-domain
└── DNS MX lookup
├── MX override? → high confidence, source: mx-lookup
├── known MX? → high confidence, source: mx-lookup
└── ISPDB → medium confidence
└── nothing → connection: null, confidence: lowA known domain costs zero requests. Everything else costs at most two: one DNS query and, if the provider isn't recognized, one Thunderbird ISPDB fetch. Both responses live in a 30-minute in-memory cache.
Options
createMailintel() returns a resolver with your own config, cache and overrides.
import { createMailintel } from "mailintel";
const lookup = createMailintel({
timeout: 3000,
cache: true,
cacheTtl: 30 * 60 * 1000,
mxLookup: "cloudflare", // or "google", or false
ispdb: true,
});
await lookup("[email protected]");
lookup.clearCache();timeoutnumberNetwork timeout per request in ms. Default 5000.
cachebooleanIn-memory domain cache. Default true.
cacheTtlnumberCache lifetime in ms. Default 30 minutes.
mxLookupboolean | stringtrue / "cloudflare", "google", or false. Cloudflare is the default.
ispdbbooleanThunderbird ISPDB fallback. Default true.
fetchtypeof fetchCustom fetch for proxies, instrumentation or tests.
providersRecord<string, Provider>Domain-keyed overrides.
mxOverridesRecord<string, Provider>MX-suffix-keyed overrides.
Result shape
One object for the library, the CLI --json output and the HTTP API.
interface MailintelResult {
email: string; // "" for domain-only input
domain: string;
provider: string | null; // "google", "microsoft", "zoho", ...
providerDisplayName: string | null;
mx: string | null; // primary MX host
type: "free" | "business" | "unknown";
isFree: boolean;
hasMx: boolean;
connection: {
imap: { host: string; port: number; security: "tls" | "starttls" | "none" };
smtp: { host: string; port: number; security: "tls" | "starttls" | "none" };
username: string;
usernameTemplate: string;
} | null;
status: "ok" | "no-mx-records" | "unknown-provider" | "lookup-disabled";
confidence: "high" | "medium" | "low";
source: "override" | "known-domain" | "mx-lookup" | null;
raw: { mx: MxRecord[] | null; dns: object | null; ispdb: string | null };
}Status & confidence
Never assume a result is usable. Branch on status; treat connection as null until it is ok.
okSettings found. connection is populated.
no-mx-recordsThe domain publishes no MX records and cannot receive mail.
unknown-providerMail servers exist, but no IMAP/SMTP mapping for them.
lookup-disabledLookups were disabled and the domain is not in the built-in map.
confidence is separate: high means a known domain or MX pattern, medium means ISPDB, low means nothing matched.source tells you which path produced it.
Username templates
Templates follow Thunderbird's ISPDB convention. username is the template with the input applied; usernameTemplate is the raw value.
%EMAILLOCALPART%userSome older and self-hosted setups
%EMAILDOMAIN%example.comRare, mostly legacy systems
Custom providers
Private mail infrastructure won't be in any public dataset. Add an override keyed by domain, or by MX suffix when you want every domain on that server covered.
const lookup = createMailintel({
providers: {
"megacorp.io": {
provider: "internal",
providerDisplayName: "MegaCorp Mail",
imap: { host: "imap.megacorp.io", port: 993, security: "tls" },
smtp: { host: "smtp.megacorp.io", port: 465, security: "tls" },
},
},
mxOverrides: {
"mail.megacorp.net": { /* same shape */ },
},
});
await lookup("[email protected]"); // no network callBrowser & edge
The library has no Node dependencies. It uses fetch and nothing else, so it works in the browser, in Cloudflare Workers and in any edge runtime. This site runs the exact same code client-side.
<script type="module">
import { mailintel } from "https://esm.sh/[email protected]";
const result = await mailintel("[email protected]");
console.log(result.provider, result.connection?.imap);
</script>mxLookup: "google" belongs on the server. The Thunderbird ISPDB also allows cross-origin requests.CLI
Same resolver, zero setup. Useful for debugging a domain without writing code.
$ npx mailintel [email protected]
[email protected]
provider google
domain acme.dev
type business
confidence high
mx smtp.google.com
source mx-lookup
IMAP imap.gmail.com:993 SSL
SMTP smtp.gmail.com:465 SSL
Username [email protected]
$ mailintel acme.dev --json # machine-readable
$ mailintel [email protected] [email protected] # multiple inputs
$ mailintel acme.dev --resolver google --no-cachePrivacy
The library sends only the domain in its DNS and ISPDB requests. This site's playground resolves in your browser; if that path is blocked, the fallback also sends the domain only. Aggregate counters store the domain, provider and outcome — never the local part, never the full address.