2026-02-21 21:06:36 +00:00
import {
DeesElement ,
customElement ,
html ,
css ,
cssManager ,
property ,
type TemplateResult ,
} from '@design.estate/dees-element' ;
import type { IEmail } from './sz-mta-list-view.js' ;
declare global {
interface HTMLElementTagNameMap {
'sz-mta-detail-view' : SzMtaDetailView ;
}
}
export interface ISmtpLogEntry {
timestamp : string ;
direction : 'client' | 'server' ;
command : string ;
responseCode? : number ;
}
export interface IConnectionInfo {
sourceIp : string ;
sourceHostname : string ;
destinationIp : string ;
destinationPort : number ;
tlsVersion : string ;
tlsCipher : string ;
authenticated : boolean ;
authMethod : string ;
authUser : string ;
}
export interface IAuthenticationResults {
spf : 'pass' | 'fail' | 'softfail' | 'neutral' | 'none' ;
spfDomain : string ;
dkim : 'pass' | 'fail' | 'none' ;
dkimDomain : string ;
dmarc : 'pass' | 'fail' | 'none' ;
dmarcPolicy : string ;
}
export interface IEmailDetail extends IEmail {
to : string ;
toList : string [ ] ;
cc? : string [ ] ;
smtpLog : ISmtpLogEntry [ ] ;
connectionInfo : IConnectionInfo ;
authenticationResults : IAuthenticationResults ;
rejectionReason? : string ;
bounceMessage? : string ;
headers : Record < string , string > ;
body : string ;
}
@customElement ( 'sz-mta-detail-view' )
export class SzMtaDetailView extends DeesElement {
public static demo = ( ) = > html `
<div style="padding: 24px; max-width: 1200px;">
<sz-mta-detail-view
.email= ${ {
id : '1' ,
direction : 'outbound' ,
status : 'delivered' ,
from : 'noreply@serve.zone' ,
to : 'user@example.com' ,
toList : [ 'user@example.com' ] ,
subject : 'Welcome to serve.zone' ,
timestamp : '2024-01-15 14:30:22' ,
messageId : '<abc123@serve.zone>' ,
size : '12.4 KB' ,
smtpLog : [
{ timestamp : '14:30:20' , direction : 'client' , command : 'EHLO mail.serve.zone' } ,
{ timestamp: '14:30:20', direction: 'server', command: '250-mail.example.com Hello', responseCode: 250 },
{ timestamp: '14:30:20', direction: 'server', command: '250-STARTTLS', responseCode: 250 },
{ timestamp: '14:30:20', direction: 'server', command: '250 SIZE 52428800', responseCode: 250 },
{ timestamp: '14:30:20', direction: 'client', command: 'STARTTLS' },
{ timestamp: '14:30:21', direction: 'server', command: '220 Ready to start TLS', responseCode: 220 },
{ timestamp: '14:30:21', direction: 'client', command: 'EHLO mail.serve.zone' },
{ timestamp: '14:30:21', direction: 'server', command: '250-mail.example.com Hello', responseCode: 250 },
{ timestamp: '14:30:21', direction: 'server', command: '250-AUTH PLAIN LOGIN', responseCode: 250 },
{ timestamp: '14:30:21', direction: 'server', command: '250 SIZE 52428800', responseCode: 250 },
{ timestamp: '14:30:21', direction: 'client', command: 'AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=' },
{ timestamp: '14:30:21', direction: 'server', command: '235 2.7.0 Authentication successful', responseCode: 235 },
{ timestamp: '14:30:21', direction: 'client', command: 'MAIL FROM:<noreply@serve.zone>' },
{ timestamp: '14:30:21', direction: 'server', command: '250 OK', responseCode: 250 },
{ timestamp: '14:30:21', direction: 'client', command: 'RCPT TO:<user@example.com>' },
{ timestamp: '14:30:21', direction: 'server', command: '250 Accepted', responseCode: 250 },
{ timestamp: '14:30:22', direction: 'client', command: 'DATA' },
{ timestamp: '14:30:22', direction: 'server', command: '354 Enter message, ending with "." on a line by itself', responseCode: 354 },
{ timestamp: '14:30:22', direction: 'client', command: '.' },
{ timestamp: '14:30:22', direction: 'server', command: '250 OK id=1pQ2rS-0003Ab-C4', responseCode: 250 },
{ timestamp: '14:30:22', direction: 'client', command: 'QUIT' },
{ timestamp: '14:30:22', direction: 'server', command: '221 mail.example.com closing connection', responseCode: 221 },
],
connectionInfo: {
sourceIp: '10.0.1.50',
sourceHostname: 'mail.serve.zone',
destinationIp: '93.184.216.34',
destinationPort: 25,
tlsVersion: 'TLSv1.3',
tlsCipher: 'TLS_AES_256_GCM_SHA384',
authenticated: true,
authMethod: 'PLAIN',
authUser: 'noreply@serve.zone',
},
authenticationResults: {
spf: 'pass',
spfDomain: 'serve.zone',
dkim: 'pass',
dkimDomain: 'serve.zone',
dmarc: 'pass',
dmarcPolicy: 'reject',
},
headers: {
'From': 'noreply@serve.zone',
'To': 'user@example.com',
'Subject': 'Welcome to serve.zone',
'Date': 'Mon, 15 Jan 2024 14:30:22 +0000',
'MIME-Version': '1.0',
'Content-Type': 'text/html; charset=UTF-8',
},
body: '<html> \\ n<head><title>Welcome</title></head> \\ n<body> \\ n <h1>Welcome to serve.zone!</h1> \\ n <p>Your account has been created successfully.</p> \\ n <a href="https://serve.zone/dashboard">Go to Dashboard</a> \\ n</body> \\ n</html>',
}}
></sz-mta-detail-view>
</div>
` ;
public static demoGroups = [ 'MTA' ] ;
@property ( { type : Object } )
public accessor email : IEmailDetail | null = null ;
public static styles = [
cssManager . defaultStyles ,
css `
:host {
display: block;
}
.header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
cursor: pointer;
transition: color 200ms ease;
}
.back-link:hover {
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.email-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.email-subject {
font-size: 24px;
font-weight: 700;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
margin: 0;
}
.badge-group {
display: flex;
gap: 8px;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 9999px;
font-size: 13px;
font-weight: 500;
}
.status-badge.delivered {
background: ${ cssManager . bdTheme ( '#dcfce7' , 'rgba(34, 197, 94, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#16a34a' , '#22c55e' ) } ;
}
.status-badge.bounced,
.status-badge.rejected {
background: ${ cssManager . bdTheme ( '#fee2e2' , 'rgba(239, 68, 68, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#dc2626' , '#ef4444' ) } ;
}
.status-badge.deferred {
background: ${ cssManager . bdTheme ( '#fef9c3' , 'rgba(250, 204, 21, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#ca8a04' , '#facc15' ) } ;
}
.status-badge.pending {
background: ${ cssManager . bdTheme ( '#dbeafe' , 'rgba(59, 130, 246, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#2563eb' , '#60a5fa' ) } ;
}
.direction-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 9999px;
font-size: 13px;
font-weight: 500;
}
.direction-badge.inbound {
background: ${ cssManager . bdTheme ( '#dcfce7' , 'rgba(34, 197, 94, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#16a34a' , '#22c55e' ) } ;
}
.direction-badge.outbound {
background: ${ cssManager . bdTheme ( '#dbeafe' , 'rgba(59, 130, 246, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#2563eb' , '#60a5fa' ) } ;
}
.content {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
@media (min-width: 1024px) {
.content {
grid-template-columns: 2fr 1fr;
}
}
.main-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 24px;
}
2026-04-07 22:27:23 +00:00
.card-header {
height: 36px;
display: flex;
align-items: center;
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
.card-heading {
flex: 1;
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.card-title {
font-weight: 500;
font-size: 13px;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
white-space: nowrap;
2026-02-21 21:06:36 +00:00
overflow: hidden;
2026-04-07 22:27:23 +00:00
text-overflow: ellipsis;
2026-02-21 21:06:36 +00:00
}
2026-04-07 22:27:23 +00:00
.card-subtitle {
font-size: 12px;
color: var(--dees-color-text-muted);
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-footer {
2026-02-21 21:06:36 +00:00
display: flex;
2026-04-07 22:27:23 +00:00
flex-direction: row;
justify-content: flex-end;
2026-02-21 21:06:36 +00:00
align-items: center;
2026-04-07 22:27:23 +00:00
gap: 0;
height: 36px;
width: 100%;
box-sizing: border-box;
2026-02-21 21:06:36 +00:00
}
2026-04-07 22:27:23 +00:00
.tile-button {
padding: 0 16px;
height: 100%;
text-align: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
background: transparent;
border: none;
border-left: 1px solid var(--dees-color-border-subtle);
color: var(--dees-color-text-muted);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}
.tile-button:first-child {
border-left: none;
}
.tile-button:hover {
background: var(--dees-color-hover);
color: var(--dees-color-text-primary);
}
.tile-button.primary {
color: ${ cssManager . bdTheme ( 'hsl(217.2 91.2% 59.8%)' , 'hsl(213.1 93.9% 67.8%)' ) } ;
2026-02-21 21:06:36 +00:00
font-weight: 600;
}
2026-04-07 22:27:23 +00:00
.tile-button.primary:hover {
background: ${ cssManager . bdTheme ( 'hsl(217.2 91.2% 59.8% / 0.08)' , 'hsl(213.1 93.9% 67.8% / 0.08)' ) } ;
color: ${ cssManager . bdTheme ( 'hsl(217.2 91.2% 50%)' , 'hsl(213.1 93.9% 75%)' ) } ;
2026-02-21 21:06:36 +00:00
}
.card-content {
padding: 16px;
}
.detail-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.detail-label {
font-size: 14px;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
flex-shrink: 0;
}
.detail-value {
font-size: 14px;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
text-align: right;
word-break: break-all;
}
.smtp-log-container {
padding: 16px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 13px;
line-height: 1.6;
max-height: 500px;
overflow-y: auto;
background: ${ cssManager . bdTheme ( '#fafafa' , '#0a0a0a' ) } ;
display: flex;
flex-direction: column;
gap: 8px;
scrollbar-width: thin;
scrollbar-color: ${ cssManager . bdTheme ( '#d4d4d8' , '#3f3f46' ) } transparent;
}
.smtp-log-container::-webkit-scrollbar {
width: 6px;
}
.smtp-log-container::-webkit-scrollbar-track {
background: transparent;
}
.smtp-log-container::-webkit-scrollbar-thumb {
background: ${ cssManager . bdTheme ( '#d4d4d8' , '#3f3f46' ) } ;
border-radius: 3px;
}
/* Phase separators */
.smtp-phase-separator {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 0;
margin: 4px 0;
}
.smtp-phase-line {
flex: 1;
height: 1px;
background: ${ cssManager . bdTheme ( '#e4e4e7' , '#27272a' ) } ;
}
.smtp-phase-label {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: ${ cssManager . bdTheme ( '#a1a1aa' , '#52525b' ) } ;
white-space: nowrap;
}
/* Chat bubbles */
.smtp-bubble {
border-radius: 8px;
padding: 10px 14px;
max-width: 70%;
}
.smtp-bubble.client {
align-self: flex-start;
background: ${ cssManager . bdTheme ( 'rgba(59, 130, 246, 0.08)' , 'rgba(59, 130, 246, 0.12)' ) } ;
border-left: 3px solid ${ cssManager . bdTheme ( '#3b82f6' , '#60a5fa' ) } ;
margin-right: auto;
}
.smtp-bubble.server {
align-self: flex-end;
background: ${ cssManager . bdTheme ( 'rgba(34, 197, 94, 0.06)' , 'rgba(34, 197, 94, 0.10)' ) } ;
border-right: 3px solid ${ cssManager . bdTheme ( '#22c55e' , '#4ade80' ) } ;
margin-left: auto;
text-align: right;
}
.smtp-bubble-command {
white-space: pre-wrap;
word-break: break-all;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.smtp-bubble-meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
font-size: 11px;
color: ${ cssManager . bdTheme ( '#a1a1aa' , '#52525b' ) } ;
}
.smtp-bubble.server .smtp-bubble-meta {
justify-content: flex-end;
}
.smtp-direction-tag {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.smtp-direction-tag.client {
color: ${ cssManager . bdTheme ( '#3b82f6' , '#60a5fa' ) } ;
}
.smtp-direction-tag.server {
color: ${ cssManager . bdTheme ( '#22c55e' , '#4ade80' ) } ;
}
/* Response code badges */
.response-code-badge {
display: inline-block;
padding: 1px 7px;
border-radius: 9999px;
font-size: 11px;
font-weight: 700;
margin-bottom: 4px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}
.response-code-badge.code-2xx {
background: ${ cssManager . bdTheme ( 'rgba(34, 197, 94, 0.15)' , 'rgba(34, 197, 94, 0.25)' ) } ;
color: ${ cssManager . bdTheme ( '#16a34a' , '#22c55e' ) } ;
}
.response-code-badge.code-3xx {
background: ${ cssManager . bdTheme ( 'rgba(59, 130, 246, 0.15)' , 'rgba(59, 130, 246, 0.25)' ) } ;
color: ${ cssManager . bdTheme ( '#2563eb' , '#60a5fa' ) } ;
}
.response-code-badge.code-4xx {
background: ${ cssManager . bdTheme ( 'rgba(250, 204, 21, 0.15)' , 'rgba(250, 204, 21, 0.25)' ) } ;
color: ${ cssManager . bdTheme ( '#ca8a04' , '#facc15' ) } ;
}
.response-code-badge.code-5xx {
background: ${ cssManager . bdTheme ( 'rgba(239, 68, 68, 0.15)' , 'rgba(239, 68, 68, 0.25)' ) } ;
color: ${ cssManager . bdTheme ( '#dc2626' , '#ef4444' ) } ;
}
2026-04-07 22:27:23 +00:00
/* SMTP metadata banner — sits inside content, above the log */
2026-02-21 21:06:36 +00:00
.smtp-header-subtitle {
2026-04-07 22:27:23 +00:00
padding: 10px 16px;
font-size: 12px;
color: var(--dees-color-text-muted);
border-bottom: 1px solid var(--dees-color-border-subtle);
font-family: monospace;
2026-02-21 21:06:36 +00:00
}
.smtp-direction-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.smtp-direction-badge.inbound {
background: ${ cssManager . bdTheme ( 'rgba(34, 197, 94, 0.12)' , 'rgba(34, 197, 94, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#16a34a' , '#22c55e' ) } ;
}
.smtp-direction-badge.outbound {
background: ${ cssManager . bdTheme ( 'rgba(59, 130, 246, 0.12)' , 'rgba(59, 130, 246, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#2563eb' , '#60a5fa' ) } ;
}
.email-body-container {
padding: 16px;
font-family: monospace;
font-size: 13px;
max-height: 500px;
overflow-y: auto;
background: ${ cssManager . bdTheme ( '#fafafa' , '#0a0a0a' ) } ;
white-space: pre-wrap;
word-break: break-all;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.tls-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background: ${ cssManager . bdTheme ( '#dcfce7' , 'rgba(34, 197, 94, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#16a34a' , '#22c55e' ) } ;
}
.auth-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid ${ cssManager . bdTheme ( '#f4f4f5' , '#27272a' ) } ;
}
.auth-row:last-child {
border-bottom: none;
}
.auth-label {
font-size: 14px;
font-weight: 500;
color: ${ cssManager . bdTheme ( '#18181b' , '#fafafa' ) } ;
}
.auth-domain {
font-size: 12px;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
margin-left: 8px;
}
.auth-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.auth-badge.pass {
background: ${ cssManager . bdTheme ( '#dcfce7' , 'rgba(34, 197, 94, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#16a34a' , '#22c55e' ) } ;
}
.auth-badge.fail {
background: ${ cssManager . bdTheme ( '#fee2e2' , 'rgba(239, 68, 68, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#dc2626' , '#ef4444' ) } ;
}
.auth-badge.softfail,
.auth-badge.neutral,
.auth-badge.none {
background: ${ cssManager . bdTheme ( '#fef9c3' , 'rgba(250, 204, 21, 0.2)' ) } ;
color: ${ cssManager . bdTheme ( '#ca8a04' , '#facc15' ) } ;
}
2026-04-07 22:27:23 +00:00
dees-tile.rejection-card::part(outer) {
2026-02-21 21:06:36 +00:00
border-color: ${ cssManager . bdTheme ( '#fecaca' , 'rgba(239, 68, 68, 0.3)' ) } ;
}
.rejection-content {
font-size: 14px;
color: ${ cssManager . bdTheme ( '#dc2626' , '#ef4444' ) } ;
}
.rejection-label {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
margin-bottom: 4px;
}
.rejection-text {
font-family: monospace;
font-size: 13px;
padding: 8px 12px;
background: ${ cssManager . bdTheme ( '#fef2f2' , 'rgba(239, 68, 68, 0.1)' ) } ;
border-radius: 4px;
margin-bottom: 12px;
color: ${ cssManager . bdTheme ( '#991b1b' , '#fca5a5' ) } ;
}
.rejection-text:last-child {
margin-bottom: 0;
}
.no-email {
padding: 48px 24px;
text-align: center;
color: ${ cssManager . bdTheme ( '#71717a' , '#a1a1aa' ) } ;
}
` ,
] ;
public render ( ) : TemplateResult {
if ( ! this . email ) {
return html ` <div class="no-email">No email selected</div> ` ;
}
const email = this . email ;
return html `
<div class="header">
<div class="back-link" @click= ${ ( ) = > this . handleBack ( ) } >
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
Back to Emails
</div>
</div>
<div class="email-header">
<h1 class="email-subject"> ${ email . subject } </h1>
<div class="badge-group">
<span class="status-badge ${ email . status } "> ${ email . status } </span>
<span class="direction-badge ${ email . direction } "> ${ email . direction } </span>
</div>
</div>
<div class="content">
<div class="main-content">
<!-- Email Metadata -->
2026-04-07 22:27:23 +00:00
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Email Metadata</span>
</div>
2026-02-21 21:06:36 +00:00
</div>
<div class="card-content">
<div class="detail-list">
<div class="detail-item">
<span class="detail-label">From</span>
<span class="detail-value"> ${ email . from } </span>
</div>
<div class="detail-item">
<span class="detail-label">To</span>
<span class="detail-value"> ${ email . toList . join ( ', ' ) } </span>
</div>
${ email . cc && email . cc . length > 0 ? html `
<div class="detail-item">
<span class="detail-label">CC</span>
<span class="detail-value"> ${ email . cc . join ( ', ' ) } </span>
</div>
` : '' }
<div class="detail-item">
<span class="detail-label">Subject</span>
<span class="detail-value"> ${ email . subject } </span>
</div>
<div class="detail-item">
<span class="detail-label">Date</span>
<span class="detail-value"> ${ email . timestamp } </span>
</div>
<div class="detail-item">
<span class="detail-label">Message ID</span>
<span class="detail-value"> ${ email . messageId } </span>
</div>
<div class="detail-item">
<span class="detail-label">Size</span>
<span class="detail-value"> ${ email . size } </span>
</div>
</div>
</div>
2026-04-07 22:27:23 +00:00
</dees-tile>
2026-02-21 21:06:36 +00:00
<!-- SMTP Transaction Log -->
2026-04-07 22:27:23 +00:00
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">SMTP Transaction Log</span>
<span class="smtp-direction-badge ${ email . direction } "> ${ email . direction } </span>
2026-02-21 21:06:36 +00:00
</div>
2026-04-07 22:27:23 +00:00
</div>
<div class="smtp-header-subtitle">
${ email . direction === 'outbound'
? ` ${ email . connectionInfo . sourceHostname } → ${ email . connectionInfo . destinationIp } : ${ email . connectionInfo . destinationPort } `
: ` ${ email . connectionInfo . sourceIp } → ${ email . connectionInfo . sourceHostname } : ${ email . connectionInfo . destinationPort } `
}
</div>
${ this . renderSmtpLog ( email ) }
<div slot="footer" class="card-footer">
<button class="tile-button" @click= ${ ( ) = > this . copySmtpLog ( ) } >
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2026-02-21 21:06:36 +00:00
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
Copy Log
</button>
</div>
2026-04-07 22:27:23 +00:00
</dees-tile>
2026-02-21 21:06:36 +00:00
<!-- Email Body -->
2026-04-07 22:27:23 +00:00
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Email Body (Escaped)</span>
<span class="card-subtitle">Raw content — HTML is not rendered</span>
2026-02-21 21:06:36 +00:00
</div>
</div>
<pre class="email-body-container"> ${ email . body } </pre>
2026-04-07 22:27:23 +00:00
</dees-tile>
2026-02-21 21:06:36 +00:00
</div>
<div class="sidebar">
<!-- Connection Info -->
2026-04-07 22:27:23 +00:00
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Connection Info</span>
</div>
2026-02-21 21:06:36 +00:00
</div>
<div class="card-content">
<div class="detail-list">
<div class="detail-item">
<span class="detail-label">Source IP</span>
<span class="detail-value"> ${ email . connectionInfo . sourceIp } </span>
</div>
<div class="detail-item">
<span class="detail-label">Source Hostname</span>
<span class="detail-value"> ${ email . connectionInfo . sourceHostname } </span>
</div>
<div class="detail-item">
<span class="detail-label">Destination</span>
<span class="detail-value"> ${ email . connectionInfo . destinationIp } : ${ email . connectionInfo . destinationPort } </span>
</div>
<div class="detail-item">
<span class="detail-label">TLS</span>
<span class="detail-value">
${ email . connectionInfo . tlsVersion
? html ` <span class="tls-badge"> ${ email . connectionInfo . tlsVersion } </span> `
: 'None' }
</span>
</div>
${ email . connectionInfo . tlsCipher ? html `
<div class="detail-item">
<span class="detail-label">Cipher</span>
<span class="detail-value"> ${ email . connectionInfo . tlsCipher } </span>
</div>
` : '' }
<div class="detail-item">
<span class="detail-label">Authenticated</span>
<span class="detail-value"> ${ email . connectionInfo . authenticated ? 'Yes' : 'No' } </span>
</div>
${ email . connectionInfo . authenticated ? html `
<div class="detail-item">
<span class="detail-label">Auth Method</span>
<span class="detail-value"> ${ email . connectionInfo . authMethod } </span>
</div>
<div class="detail-item">
<span class="detail-label">Auth User</span>
<span class="detail-value"> ${ email . connectionInfo . authUser } </span>
</div>
` : '' }
</div>
</div>
2026-04-07 22:27:23 +00:00
</dees-tile>
2026-02-21 21:06:36 +00:00
<!-- Authentication Results -->
2026-04-07 22:27:23 +00:00
<dees-tile>
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Authentication Results</span>
</div>
2026-02-21 21:06:36 +00:00
</div>
<div class="card-content">
<div class="auth-row">
<div>
<span class="auth-label">SPF</span>
<span class="auth-domain"> ${ email . authenticationResults . spfDomain } </span>
</div>
<span class="auth-badge ${ email . authenticationResults . spf } "> ${ email . authenticationResults . spf } </span>
</div>
<div class="auth-row">
<div>
<span class="auth-label">DKIM</span>
<span class="auth-domain"> ${ email . authenticationResults . dkimDomain } </span>
</div>
<span class="auth-badge ${ email . authenticationResults . dkim } "> ${ email . authenticationResults . dkim } </span>
</div>
<div class="auth-row">
<div>
<span class="auth-label">DMARC</span>
<span class="auth-domain">policy: ${ email . authenticationResults . dmarcPolicy } </span>
</div>
<span class="auth-badge ${ email . authenticationResults . dmarc } "> ${ email . authenticationResults . dmarc } </span>
</div>
</div>
2026-04-07 22:27:23 +00:00
</dees-tile>
2026-02-21 21:06:36 +00:00
<!-- Rejection Details (conditional) -->
${ email . status === 'rejected' || email . status === 'bounced' ? html `
2026-04-07 22:27:23 +00:00
<dees-tile class="rejection-card">
<div slot="header" class="card-header">
<div class="card-heading">
<span class="card-title">Rejection Details</span>
</div>
2026-02-21 21:06:36 +00:00
</div>
<div class="card-content">
${ email . rejectionReason ? html `
<div class="rejection-label">Rejection Reason</div>
<div class="rejection-text"> ${ email . rejectionReason } </div>
` : '' }
${ email . bounceMessage ? html `
<div class="rejection-label">Bounce Message</div>
<div class="rejection-text"> ${ email . bounceMessage } </div>
` : '' }
</div>
2026-04-07 22:27:23 +00:00
</dees-tile>
2026-02-21 21:06:36 +00:00
` : '' }
</div>
</div>
` ;
}
private getResponseCodeBadgeClass ( code : number ) : string {
if ( code >= 500 ) return 'code-5xx' ;
if ( code >= 400 ) return 'code-4xx' ;
if ( code >= 300 ) return 'code-3xx' ;
return 'code-2xx' ;
}
private getSmtpPhases ( log : ISmtpLogEntry [ ] ) : Array < { phase : string ; label : string ; entries : ISmtpLogEntry [ ] } > {
const phases : Array < { phase : string ; label : string ; entries : ISmtpLogEntry [ ] } > = [ ] ;
let currentPhase = '' ;
let ehloCount = 0 ;
for ( const entry of log ) {
const cmd = entry . command . toUpperCase ( ) ;
let phase = currentPhase ;
if ( entry . direction === 'client' ) {
if ( cmd . startsWith ( 'EHLO' ) || cmd . startsWith ( 'HELO' ) ) {
ehloCount ++ ;
if ( ehloCount === 1 ) {
phase = 'connection' ;
} else {
phase = 'post-tls' ;
}
} else if ( cmd === 'STARTTLS' ) {
phase = 'tls' ;
} else if ( cmd . startsWith ( 'AUTH' ) ) {
phase = 'auth' ;
} else if ( cmd . startsWith ( 'MAIL FROM' ) || cmd . startsWith ( 'RCPT TO' ) || cmd === 'DATA' || cmd === '.' ) {
phase = 'transfer' ;
} else if ( cmd === 'QUIT' ) {
phase = 'closing' ;
}
}
// Server responses stay in the current phase
if ( entry . direction === 'server' && phase === '' ) {
phase = currentPhase || 'connection' ;
}
if ( phase === '' ) phase = 'connection' ;
if ( phase !== currentPhase ) {
currentPhase = phase ;
const labels : Record < string , string > = {
'connection' : 'Connection' ,
'tls' : 'TLS Negotiation' ,
'post-tls' : 'Post-TLS Handshake' ,
'auth' : 'Authentication' ,
'transfer' : 'Mail Transfer' ,
'closing' : 'Closing' ,
} ;
phases . push ( { phase , label : labels [ phase ] || phase , entries : [ ] } ) ;
}
if ( phases . length === 0 ) {
phases . push ( { phase : 'connection' , label : 'Connection' , entries : [ ] } ) ;
}
phases [ phases . length - 1 ] . entries . push ( entry ) ;
}
return phases ;
}
private renderSmtpLog ( email : IEmailDetail ) : TemplateResult {
const phases = this . getSmtpPhases ( email . smtpLog ) ;
return html `
<div class="smtp-log-container">
${ phases . map ( phase = > html `
<div class="smtp-phase-separator">
<div class="smtp-phase-line"></div>
<span class="smtp-phase-label"> ${ phase . label } </span>
<div class="smtp-phase-line"></div>
</div>
${ phase . entries . map ( entry = > html `
<div class="smtp-bubble ${ entry . direction } ">
${ entry . direction === 'server' && entry . responseCode ? html `
<span class="response-code-badge ${ this . getResponseCodeBadgeClass ( entry . responseCode ) } "> ${ entry . responseCode } </span>
` : '' }
<div class="smtp-bubble-command"> ${ entry . command } </div>
<div class="smtp-bubble-meta">
<span> ${ entry . timestamp } </span>
<span>·</span>
<span class="smtp-direction-tag ${ entry . direction } "> ${ entry . direction === 'client' ? 'Client' : 'Server' } </span>
</div>
</div>
` ) }
` ) }
</div>
` ;
}
private copySmtpLog() {
if ( ! this . email ) return ;
const text = this . email . smtpLog
. map ( e = > ` [ ${ e . timestamp } ] ${ e . direction === 'client' ? 'C:' : 'S:' } ${ e . command } ` )
. join ( '\n' ) ;
navigator . clipboard . writeText ( text ) ;
}
private handleBack() {
this . dispatchEvent ( new CustomEvent ( 'back' , { bubbles : true , composed : true } ) ) ;
}
}