happyDeliver/web/src/lib/components/EmailPathCard.svelte
Pierre-Olivier Mercier d53c1b1e00
All checks were successful
continuous-integration/drone/push Build is passing
tls: surface transport TLS status in email path and authentication
Parse TLS details (version, cipher, bits, cert verification) from the
Postfix Received header parenthetical and expose them per hop, rendered
as a per-hop badge in the Email Path card.

Add an x-tls Authentication-Results result: parse it when present, and
otherwise synthesize it from the inbound hop's TLS info. A negative
result (unencrypted inbound connection) applies a -10 authentication
score penalty and is shown in the Authentication card. Enable the TLS
handler in authentication_milter.

Closes: #40
2026-06-06 16:44:27 +09:00

139 lines
6.3 KiB
Svelte

<script lang="ts">
import type { ReceivedHop } from "$lib/api/types.gen";
import { theme } from "$lib/stores/theme";
interface Props {
receivedChain: ReceivedHop[];
}
let { receivedChain }: Props = $props();
// Mirror of the backend protocolIndicatesTLS (RFC 3848): the transport keyword
// gains a trailing "S" when TLS was used (ESMTPS, ESMTPSA, SMTPS, LMTPS, LMTPSA...).
function protocolIndicatesTLS(withProto: string | undefined | null): boolean {
if (!withProto) return false;
const p = withProto.trim().toUpperCase();
return p.endsWith("S") || p.endsWith("SA");
}
// RFC 3848: a trailing "A" means the sender authenticated (SMTP AUTH):
// ESMTPA, ESMTPSA, LMTPA, LMTPSA...
function protocolIndicatesAuth(withProto: string | undefined | null): boolean {
if (!withProto) return false;
return withProto.trim().toUpperCase().endsWith("A");
}
</script>
{#if receivedChain && receivedChain.length > 0}
<div class="card shadow-sm" id="email-path">
<div
class="card-header"
class:bg-white={$theme === "light"}
class:bg-dark={$theme !== "light"}
>
<h4 class="mb-0">
<i class="bi bi-pin-map me-2"></i>
Email Path
</h4>
</div>
<div class="list-group list-group-flush">
{#each receivedChain as hop, i}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">
<span class="badge bg-primary me-2">{receivedChain.length - i}</span>
{hop.reverse || "-"}
{#if hop.ip}<span class="text-muted">({hop.ip})</span>{/if}{hop.by ||
"Unknown"}
</h6>
<small class="text-muted" title={hop.timestamp}>
{hop.timestamp
? new Intl.DateTimeFormat("default", {
dateStyle: "long",
timeStyle: "short",
}).format(new Date(hop.timestamp))
: "-"}
</small>
</div>
{#if hop.with || hop.id || hop.from}
<p class="mb-1 small d-flex gap-3">
{#if hop.with}
<span>
<span class="text-muted">Protocol:</span>
<code>{hop.with}</code>
</span>
{/if}
{#if hop.id}
<span>
<span class="text-muted">ID:</span> <code>{hop.id}</code>
</span>
{/if}
{#if hop.from}
<span>
<span class="text-muted">Helo:</span> <code>{hop.from}</code>
</span>
{/if}
</p>
{/if}
<p class="mb-0 small d-flex flex-wrap align-items-center gap-3">
{#if hop.tls}
<span class="badge bg-success">
<i class="bi bi-lock-fill me-1"></i>TLS
</span>
{#if hop.tls.version}
<span>
<span class="text-muted">Version:</span>
<code>{hop.tls.version}</code>
</span>
{/if}
{#if hop.tls.cipher}
<span>
<span class="text-muted">Cipher:</span>
<code>{hop.tls.cipher}</code>
</span>
{/if}
{#if hop.tls.bits}
<span>
<span class="text-muted">Strength:</span>
<code>{hop.tls.bits} bits</code>
</span>
{/if}
{#if hop.tls.verified !== undefined}
<span
class:text-success={hop.tls.verified}
class:text-warning={!hop.tls.verified}
>
<i
class="bi {hop.tls.verified
? 'bi-patch-check-fill'
: 'bi-patch-exclamation-fill'} me-1"
></i>
{hop.tls.verified
? "Certificate trusted"
: "Certificate not trusted"}
</span>
{/if}
{:else if protocolIndicatesTLS(hop.with)}
<span class="badge bg-success">
<i class="bi bi-lock-fill me-1"></i>TLS
</span>
{:else if hop.with}
<span class="badge bg-secondary">
<i class="bi bi-unlock me-1"></i>No TLS
</span>
{:else}
<span class="badge bg-light text-muted border">
<i class="bi bi-question-circle me-1"></i>TLS unknown
</span>
{/if}
{#if protocolIndicatesAuth(hop.with)}
<span class="badge bg-info">
<i class="bi bi-person-check-fill me-1"></i>Authenticated
</span>
{/if}
</p>
</div>
{/each}
</div>
</div>
{/if}