happyDeliver/web/src/lib/components/HeloPtrMatchDisplay.svelte
Pierre-Olivier Mercier e168446b44 dns: add HELO/PTR consistency check
Compare the HELO/EHLO hostname announced by the sending server (first
Received hop) against the sender IP's PTR records, surfacing the same
signal as x-ptr/policy.ptr in Authentication-Results. Adds helo_hostname
and helo_ptr_match to DNSResults, applies a 15-point PTR sub-score
penalty on mismatch, and displays the result in a new HELO/PTR
Consistency card.
2026-06-06 16:13:34 +09:00

87 lines
3.7 KiB
Svelte

<script lang="ts">
interface Props {
heloHostname?: string;
ptrRecords?: string[];
heloPtrMatch?: boolean;
}
let { heloHostname, ptrRecords, heloPtrMatch }: Props = $props();
const normalize = (host: string) => host.replace(/\.$/, "").trim().toLowerCase();
// Local comparison, identical to the per-record badge logic below, so the
// summary alert can never contradict the individual "Match" badges.
const localMatch = $derived(
!!heloHostname &&
!!ptrRecords &&
ptrRecords.some((ptr) => normalize(heloHostname) === normalize(ptr)),
);
// Prefer the backend verdict when it is present; otherwise fall back to the
// local comparison (e.g. for results produced before helo_ptr_match existed).
const isMatch = $derived(heloPtrMatch ?? localMatch);
</script>
{#if heloHostname}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="text-muted mb-0">
<i
class="bi"
class:bi-check-circle-fill={isMatch}
class:text-success={isMatch}
class:bi-x-circle-fill={!isMatch}
class:text-danger={!isMatch}
></i>
HELO / PTR Consistency
</h5>
<span class="badge bg-secondary">HELO</span>
</div>
<div class="card-body">
<p class="card-text small text-muted mb-0">
The HELO/EHLO hostname is the name the sending server announces when it connects.
Many mail servers check that this name matches the sender IP's reverse DNS (PTR)
record. A mismatch is a common spam signal and can hurt deliverability.
</p>
<div class="mt-2">
<strong>Announced HELO:</strong> <code>{heloHostname}</code>
</div>
{#if ptrRecords && ptrRecords.length > 0}
<div class="mt-1">
<strong>PTR Hostname(s):</strong>
{#each ptrRecords as ptr}
<div class="d-flex gap-2 align-items-center mt-1">
{#if normalize(heloHostname) === normalize(ptr)}
<span class="badge bg-success">Match</span>
{:else}
<span class="badge bg-secondary">Different</span>
{/if}
<code>{ptr}</code>
</div>
{/each}
</div>
{/if}
</div>
{#if !isMatch}
<div class="list-group list-group-flush">
<div class="list-group-item">
<div class="alert alert-warning mb-0">
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>Warning:</strong> The announced HELO hostname
<code>{heloHostname}</code>
{#if ptrRecords && ptrRecords.length > 0}
does not match the sender's PTR record{ptrRecords.length > 1 ? "s" : ""}
({#each ptrRecords as ptr, i}<code>{ptr}</code>{i <
ptrRecords.length - 1
? ", "
: ""}{/each}).
{:else}
could not be matched against a PTR record.
{/if}
Configuring the HELO name to match reverse DNS improves deliverability.
</div>
</div>
</div>
{/if}
</div>
{/if}