$domain, 'selector' => $selector, 'fqdn' => $fqdn, 'record' => null, 'tags' => [], 'logo' => null, 'dmarc' => self::checkDmarc($domain), 'errors' => [], 'warnings' => [], 'valid' => false, ]; $record = self::getTxtRecord($fqdn); if ($record === null) { $report['errors'][] = 'No BIMI TXT record found at ' . $fqdn . '.'; return $report; } $report['record'] = $record; $tags = self::parseTags($record); $report['tags'] = $tags; $version = strtoupper($tags['v'] ?? ''); if ($version === '') { $report['errors'][] = 'Missing v= tag — a BIMI record must start with v=BIMI1.'; } elseif ($version !== 'BIMI1') { $report['errors'][] = 'Unexpected v= tag: "' . ($tags['v'] ?? '') . '" (expected "BIMI1").'; } $logoUrl = trim($tags['l'] ?? ''); if ($logoUrl === '') { // An empty l= is a valid "opt out / decline" signal, but usually a mistake. $report['warnings'][] = 'l= tag is empty — this domain declines to publish a logo.'; } elseif (!self::isHttpsUrl($logoUrl)) { $report['errors'][] = 'l= must be an https:// URL. Got: ' . $logoUrl; } else { $report['logo'] = self::inspectLogo($logoUrl); foreach ($report['logo']['errors'] as $e) { $report['errors'][] = $e; } foreach ($report['logo']['warnings'] as $w) { $report['warnings'][] = $w; } } $authUrl = trim($tags['a'] ?? ''); if ($authUrl === '') { $report['warnings'][] = 'No a= tag — Gmail and Apple Mail require a Verified Mark Certificate (VMC/CMC) to show the logo.'; } elseif (!self::isHttpsUrl($authUrl)) { $report['errors'][] = 'a= must be an https:// URL pointing to a PEM certificate. Got: ' . $authUrl; } // BIMI only renders when DMARC is enforced. Fold that into the verdict. $dmarc = $report['dmarc']; if (!$dmarc['found']) { $report['errors'][] = 'No DMARC record found — BIMI requires an enforced DMARC policy.'; } elseif (!$dmarc['enforced']) { $report['errors'][] = 'DMARC policy is p=' . ($dmarc['policy'] ?? 'none') . ' — BIMI requires p=quarantine or p=reject.'; } elseif ($dmarc['pct'] !== null && $dmarc['pct'] < 100) { $report['warnings'][] = 'DMARC pct=' . $dmarc['pct'] . ' — BIMI needs pct=100 (or no pct tag) to apply to all mail.'; } $report['valid'] = empty($report['errors']); return $report; } /** @return array */ private static function parseTags(string $record): array { $tags = []; foreach (explode(';', $record) as $part) { $part = trim($part); if ($part === '' || !str_contains($part, '=')) { continue; } [$key, $value] = explode('=', $part, 2); $tags[strtolower(trim($key))] = trim($value); } return $tags; } /** Downloads the SVG logo and checks the SVG Tiny PS constraints BIMI imposes. */ private static function inspectLogo(string $url): array { $logo = [ 'url' => $url, 'bytes' => null, 'mime' => null, 'title' => null, 'errors' => [], 'warnings' => [], ]; $fetch = self::fetch($url); if ($fetch['error'] !== null) { $logo['errors'][] = 'Could not fetch logo (' . $url . '): ' . $fetch['error']; return $logo; } $body = $fetch['body']; $logo['bytes'] = strlen($body); $logo['mime'] = $fetch['mime']; if ($logo['bytes'] > self::MAX_SVG_BYTES) { $logo['errors'][] = sprintf( 'Logo is %.1f KB — BIMI requires the SVG to be under 32 KB.', $logo['bytes'] / 1024 ); } if (!str_contains($body, ' attributes to check the required Tiny PS profile. if (preg_match('/]*>/is', $body, $m)) { $svgTag = $m[0]; if (!preg_match('/baseProfile\s*=\s*["\']tiny-ps["\']/i', $svgTag)) { $logo['errors'][] = 'SVG is missing baseProfile="tiny-ps" — BIMI requires the SVG Tiny Portable/Secure profile.'; } if (!preg_match('/version\s*=\s*["\']1\.2["\']/i', $svgTag)) { $logo['warnings'][] = 'SVG root should declare version="1.2" for the Tiny profile.'; } if (preg_match('/\b(x|y|width|height)\s*=/i', $svgTag)) { $logo['warnings'][] = 'SVG root should not have x/y/width/height — use a square viewBox instead.'; } if (!preg_match('/viewBox\s*=\s*["\']\s*0\s+0\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s*["\']/i', $svgTag, $vb)) { $logo['warnings'][] = 'SVG should have a square viewBox starting at "0 0".'; } elseif (abs((float) $vb[1] - (float) $vb[2]) > 0.01) { $logo['warnings'][] = 'viewBox is not square (' . $vb[1] . '×' . $vb[2] . ') — BIMI logos must be square.'; } } // Forbidden constructs in the Tiny PS profile. if (preg_match('/ element, which is not allowed in the Tiny PS profile.'; } if (preg_match('/xlink:href|(?(.*?)<\/title>/is', $body, $m)) { $logo['title'] = trim(html_entity_decode(strip_tags($m[1]))); } else { $logo['warnings'][] = 'SVG has no element — recommended so the logo has an accessible name.'; } return $logo; } /** Reads the domain's DMARC record and decides whether it is at enforcement. */ private static function checkDmarc(string $domain): array { $record = self::getTxtRecord('_dmarc.' . $domain); $out = ['found' => false, 'record' => null, 'policy' => null, 'pct' => null, 'enforced' => false]; if ($record === null || stripos($record, 'v=DMARC1') === false) { return $out; } $out['found'] = true; $out['record'] = $record; $tags = self::parseTags($record); $out['policy'] = strtolower($tags['p'] ?? 'none'); if (isset($tags['pct']) && is_numeric($tags['pct'])) { $out['pct'] = (int) $tags['pct']; } $out['enforced'] = in_array($out['policy'], ['quarantine', 'reject'], true); return $out; } private static function isHttpsUrl(string $url): bool { return (bool) preg_match('#^https://[^\s/]+#i', $url); } private static function fetch(string $url): array { $ctx = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => 8, 'user_agent' => 'medowar-bimi-check/1.0', 'max_redirects' => 3, 'ignore_errors' => true, ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $stream = @fopen($url, 'rb', false, $ctx); if ($stream === false) { return ['body' => '', 'mime' => null, 'error' => 'connection failed or TLS error']; } $meta = stream_get_meta_data($stream); $status = self::statusFromHeaders($meta['wrapper_data'] ?? []); $mime = self::headerValue($meta['wrapper_data'] ?? [], 'content-type'); $body = @stream_get_contents($stream, self::MAX_FETCH_BYTES); fclose($stream); if ($status !== null && $status >= 400) { return ['body' => '', 'mime' => $mime, 'error' => 'HTTP ' . $status]; } if ($body === false || $body === '') { return ['body' => '', 'mime' => $mime, 'error' => 'empty response']; } return ['body' => $body, 'mime' => $mime, 'error' => null]; } /** @param array<int,string> $headers */ private static function statusFromHeaders(array $headers): ?int { foreach ($headers as $h) { if (preg_match('#^HTTP/\S+\s+(\d{3})#', $h, $m)) { $status = (int) $m[1]; } } return $status ?? null; } /** @param array<int,string> $headers */ private static function headerValue(array $headers, string $name): ?string { foreach ($headers as $h) { if (stripos($h, $name . ':') === 0) { return trim(substr($h, strlen($name) + 1)); } } return null; } private static function getTxtRecord(string $fqdn): ?string { foreach ((@dns_get_record($fqdn, DNS_TXT) ?: []) as $r) { $txt = $r['txt'] ?? (isset($r['entries']) ? implode('', $r['entries']) : ''); if ($txt !== '') { return $txt; } } return null; } } $domain = trim((string) ($_GET['domain'] ?? '')); $selector = trim((string) ($_GET['selector'] ?? '')); $report = null; $inputError = null; if ($selector === '') { $selector = 'default'; } if ($domain !== '') { // Accept a bare hostname; strip scheme/path if a URL was pasted. $domain = preg_replace('#^\w+://#', '', $domain); $domain = explode('/', $domain)[0]; $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).'; } elseif (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9._-]{0,127})?$/', $selector)) { $inputError = 'Please enter a valid selector (letters, digits, dot, dash, underscore).'; } else { $report = (new BimiChecker())->check($domain, $selector); } } $h = 'htmlspecialchars'; ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>BIMI Record Checker

🎨 BIMI Record Checker

Looks up the BIMI TXT record for a domain (selector._bimi.domain, default selector default), parses its tags, fetches the SVG logo to validate the SVG Tiny PS profile, and confirms DMARC is enforced — the precondition for a logo to display.
— valid, displayable BIMI record.
— BIMI record is missing or will not display.
⚠️

BIMI record

BIMI logo

KB
'Version', 'l' => 'Logo URL (SVG)', 'a' => 'Authority (VMC/CMC)']; foreach ($report['tags'] as $tag => $value): ?>
TagMeaningValue
(empty)' : $h($value) ?>

DMARC prerequisite

Policy p=

No DMARC record found at _dmarc..