@serve.zone/catalog 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,933 @@
1
+ import {
2
+ DeesElement,
3
+ customElement,
4
+ html,
5
+ css,
6
+ cssManager,
7
+ property,
8
+ type TemplateResult,
9
+ } from '@design.estate/dees-element';
10
+
11
+ import type { IEmail } from './sz-mta-list-view.js';
12
+
13
+ declare global {
14
+ interface HTMLElementTagNameMap {
15
+ 'sz-mta-detail-view': SzMtaDetailView;
16
+ }
17
+ }
18
+
19
+ export interface ISmtpLogEntry {
20
+ timestamp: string;
21
+ direction: 'client' | 'server';
22
+ command: string;
23
+ responseCode?: number;
24
+ }
25
+
26
+ export interface IConnectionInfo {
27
+ sourceIp: string;
28
+ sourceHostname: string;
29
+ destinationIp: string;
30
+ destinationPort: number;
31
+ tlsVersion: string;
32
+ tlsCipher: string;
33
+ authenticated: boolean;
34
+ authMethod: string;
35
+ authUser: string;
36
+ }
37
+
38
+ export interface IAuthenticationResults {
39
+ spf: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none';
40
+ spfDomain: string;
41
+ dkim: 'pass' | 'fail' | 'none';
42
+ dkimDomain: string;
43
+ dmarc: 'pass' | 'fail' | 'none';
44
+ dmarcPolicy: string;
45
+ }
46
+
47
+ export interface IEmailDetail extends IEmail {
48
+ to: string;
49
+ toList: string[];
50
+ cc?: string[];
51
+ smtpLog: ISmtpLogEntry[];
52
+ connectionInfo: IConnectionInfo;
53
+ authenticationResults: IAuthenticationResults;
54
+ rejectionReason?: string;
55
+ bounceMessage?: string;
56
+ headers: Record<string, string>;
57
+ body: string;
58
+ }
59
+
60
+ @customElement('sz-mta-detail-view')
61
+ export class SzMtaDetailView extends DeesElement {
62
+ public static demo = () => html`
63
+ <div style="padding: 24px; max-width: 1200px;">
64
+ <sz-mta-detail-view
65
+ .email=${{
66
+ id: '1',
67
+ direction: 'outbound',
68
+ status: 'delivered',
69
+ from: 'noreply@serve.zone',
70
+ to: 'user@example.com',
71
+ toList: ['user@example.com'],
72
+ subject: 'Welcome to serve.zone',
73
+ timestamp: '2024-01-15 14:30:22',
74
+ messageId: '<abc123@serve.zone>',
75
+ size: '12.4 KB',
76
+ smtpLog: [
77
+ { timestamp: '14:30:20', direction: 'client', command: 'EHLO mail.serve.zone' },
78
+ { timestamp: '14:30:20', direction: 'server', command: '250-mail.example.com Hello', responseCode: 250 },
79
+ { timestamp: '14:30:20', direction: 'server', command: '250-STARTTLS', responseCode: 250 },
80
+ { timestamp: '14:30:20', direction: 'server', command: '250 SIZE 52428800', responseCode: 250 },
81
+ { timestamp: '14:30:20', direction: 'client', command: 'STARTTLS' },
82
+ { timestamp: '14:30:21', direction: 'server', command: '220 Ready to start TLS', responseCode: 220 },
83
+ { timestamp: '14:30:21', direction: 'client', command: 'EHLO mail.serve.zone' },
84
+ { timestamp: '14:30:21', direction: 'server', command: '250-mail.example.com Hello', responseCode: 250 },
85
+ { timestamp: '14:30:21', direction: 'server', command: '250-AUTH PLAIN LOGIN', responseCode: 250 },
86
+ { timestamp: '14:30:21', direction: 'server', command: '250 SIZE 52428800', responseCode: 250 },
87
+ { timestamp: '14:30:21', direction: 'client', command: 'AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=' },
88
+ { timestamp: '14:30:21', direction: 'server', command: '235 2.7.0 Authentication successful', responseCode: 235 },
89
+ { timestamp: '14:30:21', direction: 'client', command: 'MAIL FROM:<noreply@serve.zone>' },
90
+ { timestamp: '14:30:21', direction: 'server', command: '250 OK', responseCode: 250 },
91
+ { timestamp: '14:30:21', direction: 'client', command: 'RCPT TO:<user@example.com>' },
92
+ { timestamp: '14:30:21', direction: 'server', command: '250 Accepted', responseCode: 250 },
93
+ { timestamp: '14:30:22', direction: 'client', command: 'DATA' },
94
+ { timestamp: '14:30:22', direction: 'server', command: '354 Enter message, ending with "." on a line by itself', responseCode: 354 },
95
+ { timestamp: '14:30:22', direction: 'client', command: '.' },
96
+ { timestamp: '14:30:22', direction: 'server', command: '250 OK id=1pQ2rS-0003Ab-C4', responseCode: 250 },
97
+ { timestamp: '14:30:22', direction: 'client', command: 'QUIT' },
98
+ { timestamp: '14:30:22', direction: 'server', command: '221 mail.example.com closing connection', responseCode: 221 },
99
+ ],
100
+ connectionInfo: {
101
+ sourceIp: '10.0.1.50',
102
+ sourceHostname: 'mail.serve.zone',
103
+ destinationIp: '93.184.216.34',
104
+ destinationPort: 25,
105
+ tlsVersion: 'TLSv1.3',
106
+ tlsCipher: 'TLS_AES_256_GCM_SHA384',
107
+ authenticated: true,
108
+ authMethod: 'PLAIN',
109
+ authUser: 'noreply@serve.zone',
110
+ },
111
+ authenticationResults: {
112
+ spf: 'pass',
113
+ spfDomain: 'serve.zone',
114
+ dkim: 'pass',
115
+ dkimDomain: 'serve.zone',
116
+ dmarc: 'pass',
117
+ dmarcPolicy: 'reject',
118
+ },
119
+ headers: {
120
+ 'From': 'noreply@serve.zone',
121
+ 'To': 'user@example.com',
122
+ 'Subject': 'Welcome to serve.zone',
123
+ 'Date': 'Mon, 15 Jan 2024 14:30:22 +0000',
124
+ 'MIME-Version': '1.0',
125
+ 'Content-Type': 'text/html; charset=UTF-8',
126
+ },
127
+ 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>',
128
+ }}
129
+ ></sz-mta-detail-view>
130
+ </div>
131
+ `;
132
+
133
+ public static demoGroups = ['MTA'];
134
+
135
+ @property({ type: Object })
136
+ public accessor email: IEmailDetail | null = null;
137
+
138
+ public static styles = [
139
+ cssManager.defaultStyles,
140
+ css`
141
+ :host {
142
+ display: block;
143
+ }
144
+
145
+ .header {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 16px;
149
+ margin-bottom: 24px;
150
+ }
151
+
152
+ .back-link {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ gap: 6px;
156
+ font-size: 14px;
157
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
158
+ cursor: pointer;
159
+ transition: color 200ms ease;
160
+ }
161
+
162
+ .back-link:hover {
163
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
164
+ }
165
+
166
+ .email-header {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 12px;
170
+ margin-bottom: 24px;
171
+ }
172
+
173
+ .email-subject {
174
+ font-size: 24px;
175
+ font-weight: 700;
176
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
177
+ margin: 0;
178
+ }
179
+
180
+ .badge-group {
181
+ display: flex;
182
+ gap: 8px;
183
+ }
184
+
185
+ .status-badge {
186
+ display: inline-flex;
187
+ align-items: center;
188
+ padding: 4px 12px;
189
+ border-radius: 9999px;
190
+ font-size: 13px;
191
+ font-weight: 500;
192
+ }
193
+
194
+ .status-badge.delivered {
195
+ background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
196
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
197
+ }
198
+
199
+ .status-badge.bounced,
200
+ .status-badge.rejected {
201
+ background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
202
+ color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
203
+ }
204
+
205
+ .status-badge.deferred {
206
+ background: ${cssManager.bdTheme('#fef9c3', 'rgba(250, 204, 21, 0.2)')};
207
+ color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
208
+ }
209
+
210
+ .status-badge.pending {
211
+ background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
212
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
213
+ }
214
+
215
+ .direction-badge {
216
+ display: inline-flex;
217
+ align-items: center;
218
+ gap: 4px;
219
+ padding: 4px 12px;
220
+ border-radius: 9999px;
221
+ font-size: 13px;
222
+ font-weight: 500;
223
+ }
224
+
225
+ .direction-badge.inbound {
226
+ background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
227
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
228
+ }
229
+
230
+ .direction-badge.outbound {
231
+ background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
232
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
233
+ }
234
+
235
+ .content {
236
+ display: grid;
237
+ grid-template-columns: 1fr;
238
+ gap: 24px;
239
+ }
240
+
241
+ @media (min-width: 1024px) {
242
+ .content {
243
+ grid-template-columns: 2fr 1fr;
244
+ }
245
+ }
246
+
247
+ .main-content {
248
+ display: flex;
249
+ flex-direction: column;
250
+ gap: 24px;
251
+ }
252
+
253
+ .sidebar {
254
+ display: flex;
255
+ flex-direction: column;
256
+ gap: 24px;
257
+ }
258
+
259
+ .card {
260
+ background: ${cssManager.bdTheme('#ffffff', '#09090b')};
261
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
262
+ border-radius: 8px;
263
+ overflow: hidden;
264
+ }
265
+
266
+ .card-header {
267
+ display: flex;
268
+ justify-content: space-between;
269
+ align-items: center;
270
+ padding: 16px;
271
+ border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
272
+ }
273
+
274
+ .card-title {
275
+ font-size: 16px;
276
+ font-weight: 600;
277
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
278
+ }
279
+
280
+ .card-subtitle {
281
+ font-size: 13px;
282
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
283
+ margin-top: 2px;
284
+ }
285
+
286
+ .card-content {
287
+ padding: 16px;
288
+ }
289
+
290
+ .detail-list {
291
+ display: flex;
292
+ flex-direction: column;
293
+ gap: 12px;
294
+ }
295
+
296
+ .detail-item {
297
+ display: flex;
298
+ justify-content: space-between;
299
+ align-items: flex-start;
300
+ }
301
+
302
+ .detail-label {
303
+ font-size: 14px;
304
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
305
+ flex-shrink: 0;
306
+ }
307
+
308
+ .detail-value {
309
+ font-size: 14px;
310
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
311
+ text-align: right;
312
+ word-break: break-all;
313
+ }
314
+
315
+ .smtp-log-container {
316
+ padding: 16px;
317
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
318
+ font-size: 13px;
319
+ line-height: 1.6;
320
+ max-height: 500px;
321
+ overflow-y: auto;
322
+ background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
323
+ display: flex;
324
+ flex-direction: column;
325
+ gap: 8px;
326
+ scrollbar-width: thin;
327
+ scrollbar-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')} transparent;
328
+ }
329
+
330
+ .smtp-log-container::-webkit-scrollbar {
331
+ width: 6px;
332
+ }
333
+
334
+ .smtp-log-container::-webkit-scrollbar-track {
335
+ background: transparent;
336
+ }
337
+
338
+ .smtp-log-container::-webkit-scrollbar-thumb {
339
+ background: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
340
+ border-radius: 3px;
341
+ }
342
+
343
+ /* Phase separators */
344
+ .smtp-phase-separator {
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 12px;
348
+ padding: 4px 0;
349
+ margin: 4px 0;
350
+ }
351
+
352
+ .smtp-phase-line {
353
+ flex: 1;
354
+ height: 1px;
355
+ background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
356
+ }
357
+
358
+ .smtp-phase-label {
359
+ font-size: 10px;
360
+ font-weight: 600;
361
+ text-transform: uppercase;
362
+ letter-spacing: 0.5px;
363
+ color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
364
+ white-space: nowrap;
365
+ }
366
+
367
+ /* Chat bubbles */
368
+ .smtp-bubble {
369
+ border-radius: 8px;
370
+ padding: 10px 14px;
371
+ max-width: 70%;
372
+ }
373
+
374
+ .smtp-bubble.client {
375
+ align-self: flex-start;
376
+ background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.08)', 'rgba(59, 130, 246, 0.12)')};
377
+ border-left: 3px solid ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
378
+ margin-right: auto;
379
+ }
380
+
381
+ .smtp-bubble.server {
382
+ align-self: flex-end;
383
+ background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.06)', 'rgba(34, 197, 94, 0.10)')};
384
+ border-right: 3px solid ${cssManager.bdTheme('#22c55e', '#4ade80')};
385
+ margin-left: auto;
386
+ text-align: right;
387
+ }
388
+
389
+ .smtp-bubble-command {
390
+ white-space: pre-wrap;
391
+ word-break: break-all;
392
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
393
+ }
394
+
395
+ .smtp-bubble-meta {
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 6px;
399
+ margin-top: 4px;
400
+ font-size: 11px;
401
+ color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
402
+ }
403
+
404
+ .smtp-bubble.server .smtp-bubble-meta {
405
+ justify-content: flex-end;
406
+ }
407
+
408
+ .smtp-direction-tag {
409
+ font-size: 10px;
410
+ font-weight: 600;
411
+ text-transform: uppercase;
412
+ letter-spacing: 0.3px;
413
+ }
414
+
415
+ .smtp-direction-tag.client {
416
+ color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
417
+ }
418
+
419
+ .smtp-direction-tag.server {
420
+ color: ${cssManager.bdTheme('#22c55e', '#4ade80')};
421
+ }
422
+
423
+ /* Response code badges */
424
+ .response-code-badge {
425
+ display: inline-block;
426
+ padding: 1px 7px;
427
+ border-radius: 9999px;
428
+ font-size: 11px;
429
+ font-weight: 700;
430
+ margin-bottom: 4px;
431
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
432
+ }
433
+
434
+ .response-code-badge.code-2xx {
435
+ background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.15)', 'rgba(34, 197, 94, 0.25)')};
436
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
437
+ }
438
+
439
+ .response-code-badge.code-3xx {
440
+ background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.15)', 'rgba(59, 130, 246, 0.25)')};
441
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
442
+ }
443
+
444
+ .response-code-badge.code-4xx {
445
+ background: ${cssManager.bdTheme('rgba(250, 204, 21, 0.15)', 'rgba(250, 204, 21, 0.25)')};
446
+ color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
447
+ }
448
+
449
+ .response-code-badge.code-5xx {
450
+ background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.15)', 'rgba(239, 68, 68, 0.25)')};
451
+ color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
452
+ }
453
+
454
+ /* Copy button */
455
+ .smtp-copy-button {
456
+ display: inline-flex;
457
+ align-items: center;
458
+ gap: 6px;
459
+ padding: 6px 12px;
460
+ border-radius: 6px;
461
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
462
+ background: transparent;
463
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
464
+ font-size: 12px;
465
+ font-weight: 500;
466
+ cursor: pointer;
467
+ transition: all 150ms ease;
468
+ }
469
+
470
+ .smtp-copy-button:hover {
471
+ background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
472
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
473
+ border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
474
+ }
475
+
476
+ /* Header subtitle enhancements */
477
+ .smtp-header-subtitle {
478
+ font-size: 13px;
479
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
480
+ margin-top: 2px;
481
+ display: flex;
482
+ align-items: center;
483
+ gap: 8px;
484
+ flex-wrap: wrap;
485
+ }
486
+
487
+ .smtp-direction-badge {
488
+ display: inline-flex;
489
+ align-items: center;
490
+ padding: 2px 8px;
491
+ border-radius: 9999px;
492
+ font-size: 11px;
493
+ font-weight: 600;
494
+ text-transform: uppercase;
495
+ letter-spacing: 0.3px;
496
+ }
497
+
498
+ .smtp-direction-badge.inbound {
499
+ background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.12)', 'rgba(34, 197, 94, 0.2)')};
500
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
501
+ }
502
+
503
+ .smtp-direction-badge.outbound {
504
+ background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.12)', 'rgba(59, 130, 246, 0.2)')};
505
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
506
+ }
507
+
508
+ .email-body-container {
509
+ padding: 16px;
510
+ font-family: monospace;
511
+ font-size: 13px;
512
+ max-height: 500px;
513
+ overflow-y: auto;
514
+ background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
515
+ white-space: pre-wrap;
516
+ word-break: break-all;
517
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
518
+ }
519
+
520
+ .tls-badge {
521
+ display: inline-flex;
522
+ align-items: center;
523
+ padding: 2px 8px;
524
+ border-radius: 4px;
525
+ font-size: 12px;
526
+ font-weight: 500;
527
+ background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
528
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
529
+ }
530
+
531
+ .auth-row {
532
+ display: flex;
533
+ justify-content: space-between;
534
+ align-items: center;
535
+ padding: 8px 0;
536
+ border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
537
+ }
538
+
539
+ .auth-row:last-child {
540
+ border-bottom: none;
541
+ }
542
+
543
+ .auth-label {
544
+ font-size: 14px;
545
+ font-weight: 500;
546
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
547
+ }
548
+
549
+ .auth-domain {
550
+ font-size: 12px;
551
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
552
+ margin-left: 8px;
553
+ }
554
+
555
+ .auth-badge {
556
+ display: inline-flex;
557
+ align-items: center;
558
+ padding: 2px 8px;
559
+ border-radius: 9999px;
560
+ font-size: 12px;
561
+ font-weight: 500;
562
+ }
563
+
564
+ .auth-badge.pass {
565
+ background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
566
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
567
+ }
568
+
569
+ .auth-badge.fail {
570
+ background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
571
+ color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
572
+ }
573
+
574
+ .auth-badge.softfail,
575
+ .auth-badge.neutral,
576
+ .auth-badge.none {
577
+ background: ${cssManager.bdTheme('#fef9c3', 'rgba(250, 204, 21, 0.2)')};
578
+ color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
579
+ }
580
+
581
+ .rejection-card {
582
+ border-color: ${cssManager.bdTheme('#fecaca', 'rgba(239, 68, 68, 0.3)')};
583
+ }
584
+
585
+ .rejection-content {
586
+ font-size: 14px;
587
+ color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
588
+ }
589
+
590
+ .rejection-label {
591
+ font-size: 12px;
592
+ font-weight: 600;
593
+ text-transform: uppercase;
594
+ letter-spacing: 0.05em;
595
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
596
+ margin-bottom: 4px;
597
+ }
598
+
599
+ .rejection-text {
600
+ font-family: monospace;
601
+ font-size: 13px;
602
+ padding: 8px 12px;
603
+ background: ${cssManager.bdTheme('#fef2f2', 'rgba(239, 68, 68, 0.1)')};
604
+ border-radius: 4px;
605
+ margin-bottom: 12px;
606
+ color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
607
+ }
608
+
609
+ .rejection-text:last-child {
610
+ margin-bottom: 0;
611
+ }
612
+
613
+ .no-email {
614
+ padding: 48px 24px;
615
+ text-align: center;
616
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
617
+ }
618
+ `,
619
+ ];
620
+
621
+ public render(): TemplateResult {
622
+ if (!this.email) {
623
+ return html`<div class="no-email">No email selected</div>`;
624
+ }
625
+
626
+ const email = this.email;
627
+
628
+ return html`
629
+ <div class="header">
630
+ <div class="back-link" @click=${() => this.handleBack()}>
631
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
632
+ <polyline points="15 18 9 12 15 6"></polyline>
633
+ </svg>
634
+ Back to Emails
635
+ </div>
636
+ </div>
637
+
638
+ <div class="email-header">
639
+ <h1 class="email-subject">${email.subject}</h1>
640
+ <div class="badge-group">
641
+ <span class="status-badge ${email.status}">${email.status}</span>
642
+ <span class="direction-badge ${email.direction}">${email.direction}</span>
643
+ </div>
644
+ </div>
645
+
646
+ <div class="content">
647
+ <div class="main-content">
648
+ <!-- Email Metadata -->
649
+ <div class="card">
650
+ <div class="card-header">
651
+ <div class="card-title">Email Metadata</div>
652
+ </div>
653
+ <div class="card-content">
654
+ <div class="detail-list">
655
+ <div class="detail-item">
656
+ <span class="detail-label">From</span>
657
+ <span class="detail-value">${email.from}</span>
658
+ </div>
659
+ <div class="detail-item">
660
+ <span class="detail-label">To</span>
661
+ <span class="detail-value">${email.toList.join(', ')}</span>
662
+ </div>
663
+ ${email.cc && email.cc.length > 0 ? html`
664
+ <div class="detail-item">
665
+ <span class="detail-label">CC</span>
666
+ <span class="detail-value">${email.cc.join(', ')}</span>
667
+ </div>
668
+ ` : ''}
669
+ <div class="detail-item">
670
+ <span class="detail-label">Subject</span>
671
+ <span class="detail-value">${email.subject}</span>
672
+ </div>
673
+ <div class="detail-item">
674
+ <span class="detail-label">Date</span>
675
+ <span class="detail-value">${email.timestamp}</span>
676
+ </div>
677
+ <div class="detail-item">
678
+ <span class="detail-label">Message ID</span>
679
+ <span class="detail-value">${email.messageId}</span>
680
+ </div>
681
+ <div class="detail-item">
682
+ <span class="detail-label">Size</span>
683
+ <span class="detail-value">${email.size}</span>
684
+ </div>
685
+ </div>
686
+ </div>
687
+ </div>
688
+
689
+ <!-- SMTP Transaction Log -->
690
+ <div class="card">
691
+ <div class="card-header">
692
+ <div>
693
+ <div class="card-title">SMTP Transaction Log</div>
694
+ <div class="smtp-header-subtitle">
695
+ <span class="smtp-direction-badge ${email.direction}">${email.direction}</span>
696
+ <span>${email.direction === 'outbound'
697
+ ? `${email.connectionInfo.sourceHostname} → ${email.connectionInfo.destinationIp}:${email.connectionInfo.destinationPort}`
698
+ : `${email.connectionInfo.sourceIp} → ${email.connectionInfo.sourceHostname}:${email.connectionInfo.destinationPort}`
699
+ }</span>
700
+ </div>
701
+ </div>
702
+ <button class="smtp-copy-button" @click=${() => this.copySmtpLog()}>
703
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
704
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
705
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
706
+ </svg>
707
+ Copy Log
708
+ </button>
709
+ </div>
710
+ ${this.renderSmtpLog(email)}
711
+ </div>
712
+
713
+ <!-- Email Body -->
714
+ <div class="card">
715
+ <div class="card-header">
716
+ <div>
717
+ <div class="card-title">Email Body (Escaped)</div>
718
+ <div class="card-subtitle">Raw content — HTML is not rendered</div>
719
+ </div>
720
+ </div>
721
+ <pre class="email-body-container">${email.body}</pre>
722
+ </div>
723
+ </div>
724
+
725
+ <div class="sidebar">
726
+ <!-- Connection Info -->
727
+ <div class="card">
728
+ <div class="card-header">
729
+ <div class="card-title">Connection Info</div>
730
+ </div>
731
+ <div class="card-content">
732
+ <div class="detail-list">
733
+ <div class="detail-item">
734
+ <span class="detail-label">Source IP</span>
735
+ <span class="detail-value">${email.connectionInfo.sourceIp}</span>
736
+ </div>
737
+ <div class="detail-item">
738
+ <span class="detail-label">Source Hostname</span>
739
+ <span class="detail-value">${email.connectionInfo.sourceHostname}</span>
740
+ </div>
741
+ <div class="detail-item">
742
+ <span class="detail-label">Destination</span>
743
+ <span class="detail-value">${email.connectionInfo.destinationIp}:${email.connectionInfo.destinationPort}</span>
744
+ </div>
745
+ <div class="detail-item">
746
+ <span class="detail-label">TLS</span>
747
+ <span class="detail-value">
748
+ ${email.connectionInfo.tlsVersion
749
+ ? html`<span class="tls-badge">${email.connectionInfo.tlsVersion}</span>`
750
+ : 'None'}
751
+ </span>
752
+ </div>
753
+ ${email.connectionInfo.tlsCipher ? html`
754
+ <div class="detail-item">
755
+ <span class="detail-label">Cipher</span>
756
+ <span class="detail-value">${email.connectionInfo.tlsCipher}</span>
757
+ </div>
758
+ ` : ''}
759
+ <div class="detail-item">
760
+ <span class="detail-label">Authenticated</span>
761
+ <span class="detail-value">${email.connectionInfo.authenticated ? 'Yes' : 'No'}</span>
762
+ </div>
763
+ ${email.connectionInfo.authenticated ? html`
764
+ <div class="detail-item">
765
+ <span class="detail-label">Auth Method</span>
766
+ <span class="detail-value">${email.connectionInfo.authMethod}</span>
767
+ </div>
768
+ <div class="detail-item">
769
+ <span class="detail-label">Auth User</span>
770
+ <span class="detail-value">${email.connectionInfo.authUser}</span>
771
+ </div>
772
+ ` : ''}
773
+ </div>
774
+ </div>
775
+ </div>
776
+
777
+ <!-- Authentication Results -->
778
+ <div class="card">
779
+ <div class="card-header">
780
+ <div class="card-title">Authentication Results</div>
781
+ </div>
782
+ <div class="card-content">
783
+ <div class="auth-row">
784
+ <div>
785
+ <span class="auth-label">SPF</span>
786
+ <span class="auth-domain">${email.authenticationResults.spfDomain}</span>
787
+ </div>
788
+ <span class="auth-badge ${email.authenticationResults.spf}">${email.authenticationResults.spf}</span>
789
+ </div>
790
+ <div class="auth-row">
791
+ <div>
792
+ <span class="auth-label">DKIM</span>
793
+ <span class="auth-domain">${email.authenticationResults.dkimDomain}</span>
794
+ </div>
795
+ <span class="auth-badge ${email.authenticationResults.dkim}">${email.authenticationResults.dkim}</span>
796
+ </div>
797
+ <div class="auth-row">
798
+ <div>
799
+ <span class="auth-label">DMARC</span>
800
+ <span class="auth-domain">policy: ${email.authenticationResults.dmarcPolicy}</span>
801
+ </div>
802
+ <span class="auth-badge ${email.authenticationResults.dmarc}">${email.authenticationResults.dmarc}</span>
803
+ </div>
804
+ </div>
805
+ </div>
806
+
807
+ <!-- Rejection Details (conditional) -->
808
+ ${email.status === 'rejected' || email.status === 'bounced' ? html`
809
+ <div class="card rejection-card">
810
+ <div class="card-header">
811
+ <div class="card-title">Rejection Details</div>
812
+ </div>
813
+ <div class="card-content">
814
+ ${email.rejectionReason ? html`
815
+ <div class="rejection-label">Rejection Reason</div>
816
+ <div class="rejection-text">${email.rejectionReason}</div>
817
+ ` : ''}
818
+ ${email.bounceMessage ? html`
819
+ <div class="rejection-label">Bounce Message</div>
820
+ <div class="rejection-text">${email.bounceMessage}</div>
821
+ ` : ''}
822
+ </div>
823
+ </div>
824
+ ` : ''}
825
+ </div>
826
+ </div>
827
+ `;
828
+ }
829
+
830
+ private getResponseCodeBadgeClass(code: number): string {
831
+ if (code >= 500) return 'code-5xx';
832
+ if (code >= 400) return 'code-4xx';
833
+ if (code >= 300) return 'code-3xx';
834
+ return 'code-2xx';
835
+ }
836
+
837
+ private getSmtpPhases(log: ISmtpLogEntry[]): Array<{ phase: string; label: string; entries: ISmtpLogEntry[] }> {
838
+ const phases: Array<{ phase: string; label: string; entries: ISmtpLogEntry[] }> = [];
839
+ let currentPhase = '';
840
+ let ehloCount = 0;
841
+
842
+ for (const entry of log) {
843
+ const cmd = entry.command.toUpperCase();
844
+ let phase = currentPhase;
845
+
846
+ if (entry.direction === 'client') {
847
+ if (cmd.startsWith('EHLO') || cmd.startsWith('HELO')) {
848
+ ehloCount++;
849
+ if (ehloCount === 1) {
850
+ phase = 'connection';
851
+ } else {
852
+ phase = 'post-tls';
853
+ }
854
+ } else if (cmd === 'STARTTLS') {
855
+ phase = 'tls';
856
+ } else if (cmd.startsWith('AUTH')) {
857
+ phase = 'auth';
858
+ } else if (cmd.startsWith('MAIL FROM') || cmd.startsWith('RCPT TO') || cmd === 'DATA' || cmd === '.') {
859
+ phase = 'transfer';
860
+ } else if (cmd === 'QUIT') {
861
+ phase = 'closing';
862
+ }
863
+ }
864
+
865
+ // Server responses stay in the current phase
866
+ if (entry.direction === 'server' && phase === '') {
867
+ phase = currentPhase || 'connection';
868
+ }
869
+ if (phase === '') phase = 'connection';
870
+
871
+ if (phase !== currentPhase) {
872
+ currentPhase = phase;
873
+ const labels: Record<string, string> = {
874
+ 'connection': 'Connection',
875
+ 'tls': 'TLS Negotiation',
876
+ 'post-tls': 'Post-TLS Handshake',
877
+ 'auth': 'Authentication',
878
+ 'transfer': 'Mail Transfer',
879
+ 'closing': 'Closing',
880
+ };
881
+ phases.push({ phase, label: labels[phase] || phase, entries: [] });
882
+ }
883
+
884
+ if (phases.length === 0) {
885
+ phases.push({ phase: 'connection', label: 'Connection', entries: [] });
886
+ }
887
+ phases[phases.length - 1].entries.push(entry);
888
+ }
889
+
890
+ return phases;
891
+ }
892
+
893
+ private renderSmtpLog(email: IEmailDetail): TemplateResult {
894
+ const phases = this.getSmtpPhases(email.smtpLog);
895
+
896
+ return html`
897
+ <div class="smtp-log-container">
898
+ ${phases.map(phase => html`
899
+ <div class="smtp-phase-separator">
900
+ <div class="smtp-phase-line"></div>
901
+ <span class="smtp-phase-label">${phase.label}</span>
902
+ <div class="smtp-phase-line"></div>
903
+ </div>
904
+ ${phase.entries.map(entry => html`
905
+ <div class="smtp-bubble ${entry.direction}">
906
+ ${entry.direction === 'server' && entry.responseCode ? html`
907
+ <span class="response-code-badge ${this.getResponseCodeBadgeClass(entry.responseCode)}">${entry.responseCode}</span>
908
+ ` : ''}
909
+ <div class="smtp-bubble-command">${entry.command}</div>
910
+ <div class="smtp-bubble-meta">
911
+ <span>${entry.timestamp}</span>
912
+ <span>·</span>
913
+ <span class="smtp-direction-tag ${entry.direction}">${entry.direction === 'client' ? 'Client' : 'Server'}</span>
914
+ </div>
915
+ </div>
916
+ `)}
917
+ `)}
918
+ </div>
919
+ `;
920
+ }
921
+
922
+ private copySmtpLog() {
923
+ if (!this.email) return;
924
+ const text = this.email.smtpLog
925
+ .map(e => `[${e.timestamp}] ${e.direction === 'client' ? 'C:' : 'S:'} ${e.command}`)
926
+ .join('\n');
927
+ navigator.clipboard.writeText(text);
928
+ }
929
+
930
+ private handleBack() {
931
+ this.dispatchEvent(new CustomEvent('back', { bubbles: true, composed: true }));
932
+ }
933
+ }