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; body: string; } @customElement('sz-mta-detail-view') export class SzMtaDetailView extends DeesElement { public static demo = () => html` ', 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:' }, { timestamp: '14:30:21', direction: 'server', command: '250 OK', responseCode: 250 }, { timestamp: '14:30:21', direction: 'client', command: 'RCPT TO:' }, { 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: '\\nWelcome\\n\\n Welcome to serve.zone!\\n Your account has been created successfully.\\n Go to Dashboard\\n\\n', }} > `; 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; } .card { background: ${cssManager.bdTheme('#ffffff', '#09090b')}; border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; border-radius: 8px; overflow: hidden; } .card-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; } .card-title { font-size: 16px; font-weight: 600; color: ${cssManager.bdTheme('#18181b', '#fafafa')}; } .card-subtitle { font-size: 13px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; margin-top: 2px; } .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')}; } /* Copy button */ .smtp-copy-button { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 6px; border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; background: transparent; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 150ms ease; } .smtp-copy-button:hover { background: ${cssManager.bdTheme('#f4f4f5', '#27272a')}; color: ${cssManager.bdTheme('#18181b', '#fafafa')}; border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')}; } /* Header subtitle enhancements */ .smtp-header-subtitle { font-size: 13px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; margin-top: 2px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .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')}; } .rejection-card { 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`No email selected`; } const email = this.email; return html` this.handleBack()}> Back to Emails ${email.subject} ${email.status} ${email.direction} Email Metadata From ${email.from} To ${email.toList.join(', ')} ${email.cc && email.cc.length > 0 ? html` CC ${email.cc.join(', ')} ` : ''} Subject ${email.subject} Date ${email.timestamp} Message ID ${email.messageId} Size ${email.size} SMTP Transaction Log ${email.direction} ${email.direction === 'outbound' ? `${email.connectionInfo.sourceHostname} โ ${email.connectionInfo.destinationIp}:${email.connectionInfo.destinationPort}` : `${email.connectionInfo.sourceIp} โ ${email.connectionInfo.sourceHostname}:${email.connectionInfo.destinationPort}` } this.copySmtpLog()}> Copy Log ${this.renderSmtpLog(email)} Email Body (Escaped) Raw content โ HTML is not rendered ${email.body} Connection Info Source IP ${email.connectionInfo.sourceIp} Source Hostname ${email.connectionInfo.sourceHostname} Destination ${email.connectionInfo.destinationIp}:${email.connectionInfo.destinationPort} TLS ${email.connectionInfo.tlsVersion ? html`${email.connectionInfo.tlsVersion}` : 'None'} ${email.connectionInfo.tlsCipher ? html` Cipher ${email.connectionInfo.tlsCipher} ` : ''} Authenticated ${email.connectionInfo.authenticated ? 'Yes' : 'No'} ${email.connectionInfo.authenticated ? html` Auth Method ${email.connectionInfo.authMethod} Auth User ${email.connectionInfo.authUser} ` : ''} Authentication Results SPF ${email.authenticationResults.spfDomain} ${email.authenticationResults.spf} DKIM ${email.authenticationResults.dkimDomain} ${email.authenticationResults.dkim} DMARC policy: ${email.authenticationResults.dmarcPolicy} ${email.authenticationResults.dmarc} ${email.status === 'rejected' || email.status === 'bounced' ? html` Rejection Details ${email.rejectionReason ? html` Rejection Reason ${email.rejectionReason} ` : ''} ${email.bounceMessage ? html` Bounce Message ${email.bounceMessage} ` : ''} ` : ''} `; } 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 = { '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` ${phases.map(phase => html` ${phase.label} ${phase.entries.map(entry => html` ${entry.direction === 'server' && entry.responseCode ? html` ${entry.responseCode} ` : ''} ${entry.command} ${entry.timestamp} ยท ${entry.direction === 'client' ? 'Client' : 'Server'} `)} `)} `; } 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 })); } }
Your account has been created successfully.
${email.body}