,error:?string}> */ public array $targets = []; public bool $nullMx = false; // RFC 7505 — domain explicitly refuses mail public bool $hasMx = false; // at least one usable MX record was found public ?string $error = null; public function resolve(string $domain): void { $mx = @dns_get_record($domain, DNS_MX) ?: []; // RFC 7505 Null MX: exactly one record, preference 0, target "." (root). if (count($mx) === 1 && (int) ($mx[0]['pri'] ?? -1) === 0 && rtrim((string) ($mx[0]['target'] ?? ''), '.') === '') { $this->nullMx = true; $this->warnings[] = 'This domain publishes a Null MX record (RFC 7505: "0 .") — ' . 'it explicitly does not accept email. Senders should bounce immediately.'; return; } if (!empty($mx)) { $this->hasMx = true; // Sort by preference ascending; senders try the lowest number first. usort($mx, static fn($a, $b) => ($a['pri'] ?? 0) <=> ($b['pri'] ?? 0)); $prefs = array_map(static fn($r) => (int) ($r['pri'] ?? 0), $mx); if (count($prefs) !== count(array_unique($prefs))) { $this->warnings[] = 'Several MX records share the same preference. A sender picks ' . 'between equal-preference hosts at random, so the exact host order can vary per delivery.'; } foreach ($mx as $r) { $host = rtrim((string) ($r['target'] ?? ''), '.'); $this->targets[] = $this->buildTarget((int) ($r['pri'] ?? 0), $host, false); } return; } // No MX record → RFC 5321 implicit MX: try the domain's own A / AAAA. $implicit = $this->buildTarget(0, $domain, true); if (empty($implicit['addresses'])) { $this->error = 'No MX records and no A/AAAA records for the domain — ' . 'there is nowhere to deliver mail. Senders will return a bounce.'; return; } $this->warnings[] = 'No MX records found. Under RFC 5321 the domain\'s own address (A/AAAA) ' . 'is used as an implicit MX at preference 0.'; $this->targets[] = $implicit; } /** * Resolves one MX target to its addresses and flags common misconfigurations * (a CNAME where a hostname is required, or a target that does not resolve). */ private function buildTarget(int $pref, string $host, bool $implicit): array { $target = [ 'pref' => $pref, 'host' => $host, 'implicit' => $implicit, 'cname' => null, 'addresses' => [], 'error' => null, ]; if ($host === '') { $target['error'] = 'Empty MX target.'; return $target; } // RFC 2181 §10.3 / RFC 5321 §5.1: an MX target must be a hostname with // address records, never a CNAME. Flag it, but still follow the chain. $cname = @dns_get_record($host, DNS_CNAME) ?: []; foreach ($cname as $c) { if (($c['host'] ?? '') === $host && !empty($c['target'])) { $target['cname'] = rtrim((string) $c['target'], '.'); if (!$implicit) { $this->warnings[] = sprintf( 'MX target "%s" is a CNAME pointing to "%s". RFC 2181 forbids this; some ' . 'senders reject such records. It should be an A/AAAA hostname.', $host, $target['cname'] ); } break; } } foreach (@dns_get_record($host, DNS_A) ?: [] as $r) { if (!empty($r['ip'])) { $target['addresses'][] = ['ip' => $r['ip'], 'version' => 'IPv4']; } } foreach (@dns_get_record($host, DNS_AAAA) ?: [] as $r) { if (!empty($r['ipv6'])) { $target['addresses'][] = ['ip' => $r['ipv6'], 'version' => 'IPv6']; } } if (empty($target['addresses'])) { $target['error'] = $target['cname'] !== null ? 'Target is a CNAME and did not resolve to any address.' : 'MX host has no A/AAAA records — a sender cannot connect to it.'; } return $target; } /** @return string[] Every unique IP across all targets, for batch enrichment. */ public function allIps(): array { $ips = []; foreach ($this->targets as $t) { foreach ($t['addresses'] as $a) { $ips[] = $a['ip']; } } return array_values(array_unique($ips)); } } /** * Enriches IPs with ASN / country / company via ip-api.com's free batch endpoint. * @param string[] $ips * @return array */ function lookupIpInfo(array $ips): array { $out = []; $ips = array_values(array_unique(array_filter($ips))); if (empty($ips)) { return $out; } $fields = 'query,status,message,country,countryCode,as,asname,isp,org,reverse'; foreach (array_chunk($ips, 100) as $chunk) { $payload = array_map(static fn($ip) => ['query' => $ip, 'fields' => $fields], $chunk); $ch = curl_init('http://ip-api.com/batch'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, ]); $resp = curl_exec($ch); curl_close($ch); foreach ((json_decode((string) $resp, true) ?: []) as $item) { if (isset($item['query'])) { $out[$item['query']] = $item; } } } return $out; } $domain = trim((string) ($_GET['domain'] ?? '')); $resolver = null; $ipInfo = []; $inputError = null; if ($domain !== '') { // Accept a bare hostname; strip scheme/path if a URL was pasted, and an // email address if someone enters user@example.com. $domain = preg_replace('#^\w+://#', '', $domain); $domain = explode('/', $domain)[0]; if (str_contains($domain, '@')) { $domain = substr($domain, strrpos($domain, '@') + 1); } $domain = strtolower(trim($domain)); if (!preg_match('/^(?=.{1,253}$)([a-z0-9](-?[a-z0-9])*\.)+[a-z]{2,}$/', $domain)) { $inputError = 'Please enter a valid domain name (e.g. example.com) or an email address.'; } else { $resolver = new DeliveryResolver(); $resolver->resolve($domain); $ipInfo = lookupIpInfo($resolver->allIps()); } } ?> Mail Delivery Route Checker

📮 Mail Delivery Route Checker

Shows where a sending mail server would try to deliver email for a domain, following RFC 5321 routing: MX records sorted by preference (lowest first), each MX resolved to its A / AAAA addresses, plus Null MX (RFC 7505) and implicit MX fallback. Every address is enriched with its ASN, country and company.
nullMx): ?>
🚫 does not accept mail (Null MX).
error !== null): ?>
❌ Mail cannot be delivered to .
targets, static fn($t) => !empty($t['addresses'])); ?>
✅ Mail for would be delivered to reachable hasMx ? 'MX host' : 'implicit MX host' ?>.
❌ MX records exist but none resolve to a usable address.
warnings as $w): ?>
⚠️
error !== null): ?>
error) ?>
targets)): ?>

Delivery targets, in the order a sender tries them

targets as $i => $t): ?>
tried first implicit MX (A/AAAA) CNAME →
⚠️
$a): ?>
# IP address Type PTR (reverse) ASN Company / ISP Country
targets as $t) { foreach ($t['addresses'] as $a) { $flat[] = ['pref' => $t['pref'], 'host' => $t['host'], 'ip' => $a['ip']]; } } ?>

Connection attempt order

A sender opens an SMTP connection to these addresses in turn, moving on only when one is unreachable or defers.

  1. [pref ] →

Note: equal-preference MX hosts, and the choice between IPv4/IPv6 per host, are ultimately up to the sending MTA — so the exact order can differ between deliveries.

nullMx && $resolver->error === null): ?>
No delivery targets were found.

IP intelligence via ip-api.com (free tier).