mailcatcher-ng 1.0.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.
data/views/index.erb ADDED
@@ -0,0 +1,1706 @@
1
+ <!DOCTYPE html>
2
+ <html class="mailcatcher">
3
+ <head>
4
+ <title>MailCatcher NG</title>
5
+ <base href="<%= settings.prefix.chomp("/") %>/">
6
+ <link href="favicon.ico" rel="icon">
7
+ <script src="<%= asset_path("mailcatcher.js") %>"></script>
8
+ <!-- Tippy.js v6 for email signature tooltips -->
9
+ <script src="https://unpkg.com/@popperjs/core@2"></script>
10
+ <script src="https://unpkg.com/tippy.js@6"></script>
11
+ <link rel="stylesheet" href="https://unpkg.com/tippy.js@6/themes/light.css">
12
+ <!-- Highlight.js for syntax highlighting HTML emails -->
13
+ <script src="https://unpkg.com/highlight.js@11/highlight.min.js"></script>
14
+ <link rel="stylesheet" href="https://unpkg.com/highlight.js@11/styles/atom-one-light.min.css">
15
+ <style>
16
+ /* MailCatcher NG UI Styles - Complete inline stylesheet
17
+ This is the single source of truth for all MailCatcher NG UI styling.
18
+ No external CSS files are loaded (old Sass assets are deprecated).
19
+ All changes should be made here for modern, responsive design.
20
+ */
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
29
+ background: #ffffff;
30
+ color: #1a1a1a;
31
+ height: 100vh;
32
+ display: flex;
33
+ flex-direction: column;
34
+ }
35
+
36
+ header {
37
+ background: linear-gradient(135deg, #ffffff 0%, #f5f7fa 100%);
38
+ border-bottom: 1px solid #e8eaed;
39
+ padding: 20px 28px;
40
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
41
+ display: flex;
42
+ gap: 20px;
43
+ align-items: flex-start;
44
+ }
45
+
46
+ header > div:first-child {
47
+ flex-shrink: 0;
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: 4px;
51
+ }
52
+
53
+ header h1 {
54
+ font-size: 22px;
55
+ font-weight: 700;
56
+ margin: 0;
57
+ letter-spacing: -0.5px;
58
+ }
59
+
60
+ header h1 a {
61
+ color: #2196F3;
62
+ text-decoration: none;
63
+ transition: color 0.2s;
64
+ }
65
+
66
+ header h1 a:hover {
67
+ color: #1976D2;
68
+ }
69
+
70
+ .version-badge {
71
+ font-size: 11px;
72
+ color: #999;
73
+ font-weight: 400;
74
+ letter-spacing: 0.5px;
75
+ }
76
+
77
+ .header-controls {
78
+ display: flex;
79
+ gap: 16px;
80
+ align-items: center;
81
+ flex-wrap: wrap;
82
+ flex: 1;
83
+ }
84
+
85
+ .search-box {
86
+ flex: 0 1 440px;
87
+ position: relative;
88
+ display: flex;
89
+ align-items: center;
90
+ }
91
+
92
+ .search-box input {
93
+ width: 100%;
94
+ padding: 9px 32px 9px 36px;
95
+ border: 1px solid #e0e0e0;
96
+ border-radius: 8px;
97
+ font-size: 14px;
98
+ background: #ffffff;
99
+ transition: border-color 0.2s, box-shadow 0.2s;
100
+ }
101
+
102
+ .search-box input:focus {
103
+ outline: none;
104
+ border-color: #2196F3;
105
+ box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
106
+ }
107
+
108
+ .search-icon {
109
+ position: absolute;
110
+ left: 12px;
111
+ top: 50%;
112
+ transform: translateY(-50%);
113
+ width: 16px;
114
+ height: 16px;
115
+ color: #999;
116
+ pointer-events: none;
117
+ }
118
+
119
+ .search-clear {
120
+ position: absolute;
121
+ right: 10px;
122
+ top: 50%;
123
+ transform: translateY(-50%);
124
+ width: 16px;
125
+ height: 16px;
126
+ color: #999;
127
+ cursor: pointer;
128
+ display: none;
129
+ background: none;
130
+ border: none;
131
+ padding: 0;
132
+ transition: color 0.2s;
133
+ }
134
+
135
+ .search-clear:hover {
136
+ color: #666;
137
+ }
138
+
139
+ .search-box input:not(:placeholder-shown) ~ .search-clear {
140
+ display: block;
141
+ }
142
+
143
+ /* Hide browser's native search clear button */
144
+ .search-box input::-webkit-search-cancel-button {
145
+ display: none;
146
+ }
147
+
148
+ .attachment-filter {
149
+ display: flex;
150
+ gap: 4px;
151
+ align-items: center;
152
+ }
153
+
154
+ .attachment-filter select {
155
+ padding: 8px 12px;
156
+ border: 1px solid #e0e0e0;
157
+ border-radius: 6px;
158
+ background: #ffffff;
159
+ color: #1a1a1a;
160
+ cursor: pointer;
161
+ font-size: 13px;
162
+ transition: border-color 0.2s;
163
+ }
164
+
165
+ .attachment-filter select:hover {
166
+ border-color: #2196F3;
167
+ }
168
+
169
+ .attachment-filter select:focus {
170
+ outline: none;
171
+ border-color: #2196F3;
172
+ box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
173
+ }
174
+
175
+ .header-info {
176
+ display: flex;
177
+ gap: 16px;
178
+ align-items: center;
179
+ }
180
+
181
+ .status-badge {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 8px;
185
+ padding: 8px 14px;
186
+ background: #f0f0f0;
187
+ border-radius: 20px;
188
+ font-size: 13px;
189
+ font-weight: 500;
190
+ }
191
+
192
+ .status-badge .indicator {
193
+ width: 8px;
194
+ height: 8px;
195
+ border-radius: 50%;
196
+ background: #34a853;
197
+ animation: pulse 2s infinite;
198
+ }
199
+
200
+ @keyframes pulse {
201
+ 0%, 100% { opacity: 1; }
202
+ 50% { opacity: 0.6; }
203
+ }
204
+
205
+ .status-badge.disconnected .indicator {
206
+ background: #ea4335;
207
+ animation: none;
208
+ }
209
+
210
+ .action-buttons {
211
+ display: flex;
212
+ gap: 10px;
213
+ }
214
+
215
+ .btn {
216
+ padding: 8px 14px;
217
+ border: none;
218
+ border-radius: 6px;
219
+ font-size: 13px;
220
+ font-weight: 600;
221
+ cursor: pointer;
222
+ background: #2196F3;
223
+ color: #ffffff;
224
+ transition: all 0.2s;
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 6px;
228
+ }
229
+
230
+ .btn:hover {
231
+ background: #1976D2;
232
+ box-shadow: 0 2px 8px rgba(33, 150, 243, 0.2);
233
+ }
234
+
235
+ .btn:active {
236
+ transform: scale(0.98);
237
+ }
238
+
239
+ .btn svg {
240
+ width: 16px;
241
+ height: 16px;
242
+ }
243
+
244
+ .email-count {
245
+ font-size: 13px;
246
+ color: #5f5f5f;
247
+ padding: 8px 12px;
248
+ background: #f9f9f9;
249
+ border-radius: 6px;
250
+ }
251
+
252
+ main {
253
+ display: flex;
254
+ flex-direction: column;
255
+ flex: 1;
256
+ overflow: hidden;
257
+ position: relative;
258
+ }
259
+
260
+ #messages {
261
+ flex: 0 0 300px;
262
+ display: flex;
263
+ flex-direction: column;
264
+ background: #ffffff;
265
+ border-bottom: 1px solid #e8eaed;
266
+ overflow: hidden;
267
+ min-height: 150px;
268
+ }
269
+
270
+ #resizer {
271
+ width: 100%;
272
+ height: 8px;
273
+ background: #e8eaed;
274
+ cursor: row-resize;
275
+ flex-shrink: 0;
276
+ transition: background 0.2s;
277
+ position: relative;
278
+ z-index: 5;
279
+ user-select: none;
280
+ }
281
+
282
+ #resizer:hover {
283
+ background: #2196F3;
284
+ }
285
+
286
+ #resizer .ruler {
287
+ display: none;
288
+ }
289
+
290
+ #messages table {
291
+ width: 100%;
292
+ border-collapse: collapse;
293
+ font-size: 13px;
294
+ display: flex;
295
+ flex-direction: column;
296
+ height: 100%;
297
+ }
298
+
299
+ #messages thead {
300
+ background: #f9f9f9;
301
+ border-bottom: 1px solid #e8eaed;
302
+ position: sticky;
303
+ top: 0;
304
+ z-index: 10;
305
+ flex-shrink: 0;
306
+ display: flex;
307
+ width: 100%;
308
+ }
309
+
310
+ #messages thead tr {
311
+ display: flex;
312
+ width: 100%;
313
+ }
314
+
315
+ #messages th {
316
+ padding: 12px 16px;
317
+ text-align: left;
318
+ font-weight: 600;
319
+ color: #5f5f5f;
320
+ font-size: 12px;
321
+ text-transform: uppercase;
322
+ letter-spacing: 0.5px;
323
+ white-space: nowrap;
324
+ overflow: hidden;
325
+ text-overflow: ellipsis;
326
+ min-width: 0;
327
+ flex: 1;
328
+ }
329
+
330
+ #messages th.col-attachments {
331
+ flex: 0 0 40px;
332
+ padding: 12px 8px;
333
+ text-align: center;
334
+ }
335
+
336
+ #messages th.col-bimi {
337
+ flex: 0 0 40px;
338
+ padding: 12px 8px;
339
+ text-align: center;
340
+ }
341
+
342
+ #messages tbody {
343
+ overflow-y: auto;
344
+ flex: 1;
345
+ display: flex;
346
+ flex-direction: column;
347
+ width: 100%;
348
+ }
349
+
350
+ #messages tbody tr {
351
+ display: flex;
352
+ width: 100%;
353
+ flex-shrink: 0;
354
+ }
355
+
356
+ #messages tr {
357
+ border-bottom: 1px solid #f0f0f0;
358
+ cursor: pointer;
359
+ transition: background-color 0.15s;
360
+ }
361
+
362
+ #messages tr:hover {
363
+ background-color: #f9f9f9;
364
+ }
365
+
366
+ #messages tr.selected {
367
+ background-color: #f0f4ff;
368
+ }
369
+
370
+ #messages tr.selected:hover {
371
+ background-color: #e6ecff;
372
+ }
373
+
374
+ #messages td {
375
+ padding: 12px 16px;
376
+ overflow: hidden;
377
+ text-overflow: ellipsis;
378
+ white-space: nowrap;
379
+ min-width: 0;
380
+ text-align: left;
381
+ flex: 1;
382
+ }
383
+
384
+ #messages td.blank {
385
+ color: #999;
386
+ font-style: italic;
387
+ }
388
+
389
+ #messages td.col-attachments {
390
+ flex: 0 0 40px;
391
+ padding: 12px 8px;
392
+ text-align: center;
393
+ font-size: 16px;
394
+ overflow: visible;
395
+ white-space: normal;
396
+ }
397
+
398
+ #messages td.col-bimi {
399
+ flex: 0 0 40px;
400
+ padding: 12px 8px;
401
+ text-align: center;
402
+ background-size: contain;
403
+ background-repeat: no-repeat;
404
+ background-position: center;
405
+ overflow: visible;
406
+ display: flex;
407
+ align-items: center;
408
+ justify-content: center;
409
+ }
410
+
411
+ #messages .bimi-placeholder-icon {
412
+ width: 24px;
413
+ height: 24px;
414
+ color: #ccc;
415
+ flex-shrink: 0;
416
+ }
417
+
418
+ #messages .bimi-image {
419
+ max-width: 32px;
420
+ max-height: 32px;
421
+ flex-shrink: 0;
422
+ }
423
+
424
+ #messages td.subject-cell {
425
+ display: flex;
426
+ flex-direction: column;
427
+ justify-content: center;
428
+ gap: 2px;
429
+ padding: 10px 16px;
430
+ overflow: hidden;
431
+ }
432
+
433
+ #messages .subject-text {
434
+ overflow: hidden;
435
+ text-overflow: ellipsis;
436
+ white-space: nowrap;
437
+ font-weight: normal;
438
+ }
439
+
440
+ #messages .subject-text strong {
441
+ font-weight: 600;
442
+ color: #1a1a1a;
443
+ }
444
+
445
+ #messages .preview-text {
446
+ font-size: 12px;
447
+ color: #999;
448
+ overflow: hidden;
449
+ text-overflow: ellipsis;
450
+ white-space: nowrap;
451
+ font-weight: normal;
452
+ }
453
+
454
+ #messages td.from-cell {
455
+ display: flex;
456
+ flex-direction: column;
457
+ justify-content: center;
458
+ gap: 2px;
459
+ padding: 10px 16px;
460
+ overflow: hidden;
461
+ }
462
+
463
+ #messages .sender-text-container {
464
+ display: flex;
465
+ flex-direction: column;
466
+ gap: 2px;
467
+ overflow: hidden;
468
+ }
469
+
470
+ #messages .sender-name {
471
+ overflow: hidden;
472
+ text-overflow: ellipsis;
473
+ white-space: nowrap;
474
+ font-weight: normal;
475
+ }
476
+
477
+ #messages .sender-name strong {
478
+ font-weight: 600;
479
+ color: #1a1a1a;
480
+ }
481
+
482
+ #messages .sender-email {
483
+ font-size: 12px;
484
+ color: #999;
485
+ overflow: hidden;
486
+ text-overflow: ellipsis;
487
+ white-space: nowrap;
488
+ font-weight: normal;
489
+ }
490
+
491
+ #messages td.to-cell {
492
+ display: flex;
493
+ flex-direction: column;
494
+ justify-content: center;
495
+ gap: 2px;
496
+ padding: 10px 16px;
497
+ overflow: hidden;
498
+ }
499
+
500
+ #messages td.to-cell strong {
501
+ font-weight: 600;
502
+ color: #1a1a1a;
503
+ }
504
+
505
+ #messages .from-content {
506
+ display: flex;
507
+ align-items: center;
508
+ gap: 6px;
509
+ overflow: hidden;
510
+ }
511
+
512
+ #messages .attachment-icon {
513
+ flex-shrink: 0;
514
+ font-size: 14px;
515
+ margin-right: -2px;
516
+ }
517
+
518
+ #messages .bimi-icon {
519
+ flex-shrink: 0;
520
+ font-size: 16px;
521
+ width: 20px;
522
+ height: 20px;
523
+ border-radius: 3px;
524
+ background-size: contain;
525
+ background-repeat: no-repeat;
526
+ background-position: center;
527
+ margin-right: 4px;
528
+ }
529
+
530
+ #messages .sender-text {
531
+ overflow: hidden;
532
+ text-overflow: ellipsis;
533
+ white-space: nowrap;
534
+ }
535
+
536
+ #message {
537
+ flex: 1;
538
+ display: flex;
539
+ flex-direction: column;
540
+ background: #ffffff;
541
+ overflow: hidden;
542
+ }
543
+
544
+ #message > header {
545
+ border-bottom: 1px solid #e8eaed;
546
+ padding: 12px 28px;
547
+ background: #f9f9f9;
548
+ overflow-y: auto;
549
+ max-height: 40%;
550
+ flex-shrink: 0;
551
+ }
552
+
553
+ .metadata {
554
+ display: flex;
555
+ flex-direction: column;
556
+ gap: 20px;
557
+ margin-bottom: 0;
558
+ }
559
+
560
+ .metadata-column {
561
+ display: flex;
562
+ flex-direction: column;
563
+ gap: 12px;
564
+ }
565
+
566
+ .metadata-item {
567
+ display: grid;
568
+ grid-template-columns: 90px 1fr;
569
+ gap: 20px;
570
+ }
571
+
572
+ .metadata dt {
573
+ font-weight: 600;
574
+ color: #5f5f5f;
575
+ font-size: 10px;
576
+ text-transform: uppercase;
577
+ letter-spacing: 0.5px;
578
+ line-height: 1.2;
579
+ }
580
+
581
+ .metadata dd {
582
+ color: #1a1a1a;
583
+ font-size: 11px;
584
+ word-break: break-word;
585
+ line-height: 1.2;
586
+ }
587
+
588
+ .attachments-column {
589
+ display: none;
590
+ flex-direction: column;
591
+ gap: 8px;
592
+ min-width: 0;
593
+ }
594
+
595
+ .attachments-column.visible {
596
+ display: flex;
597
+ }
598
+
599
+ .attachments-header {
600
+ font-weight: 600;
601
+ color: #5f5f5f;
602
+ font-size: 10px;
603
+ text-transform: uppercase;
604
+ letter-spacing: 0.5px;
605
+ line-height: 1.2;
606
+ flex-shrink: 0;
607
+ }
608
+
609
+ .attachments-list {
610
+ list-style: none;
611
+ display: flex;
612
+ flex-direction: column;
613
+ gap: 6px;
614
+ max-height: 300px;
615
+ overflow-y: auto;
616
+ padding-right: 8px;
617
+ min-width: 0;
618
+ }
619
+
620
+ .attachments-list li {
621
+ display: flex;
622
+ justify-content: space-between;
623
+ align-items: flex-start;
624
+ gap: 12px;
625
+ padding: 6px 8px;
626
+ background: #f9f9f9;
627
+ border-radius: 4px;
628
+ font-size: 11px;
629
+ }
630
+
631
+ .attachments-list a {
632
+ color: #2196F3;
633
+ text-decoration: none;
634
+ flex: 1;
635
+ min-width: 0;
636
+ word-break: break-word;
637
+ }
638
+
639
+ .attachments-list a:hover {
640
+ text-decoration: underline;
641
+ }
642
+
643
+ .attachment-meta {
644
+ display: flex;
645
+ flex-direction: column;
646
+ align-items: flex-end;
647
+ gap: 2px;
648
+ white-space: nowrap;
649
+ flex-shrink: 0;
650
+ }
651
+
652
+ .attachment-size {
653
+ color: #666;
654
+ font-size: 10px;
655
+ }
656
+
657
+ .attachment-type {
658
+ color: #999;
659
+ font-size: 9px;
660
+ }
661
+
662
+ #message > header {
663
+ display: grid;
664
+ grid-template-columns: auto minmax(300px, 0.6fr) 280px;
665
+ gap: 20px;
666
+ align-items: start;
667
+ }
668
+
669
+ .views-container {
670
+ display: flex;
671
+ flex-direction: column;
672
+ gap: 8px;
673
+ order: -1;
674
+ align-items: flex-start;
675
+ }
676
+
677
+ .views {
678
+ display: flex;
679
+ gap: 16px;
680
+ flex-direction: row;
681
+ align-items: center;
682
+ width: 100%;
683
+ }
684
+
685
+ .views ul {
686
+ list-style: none;
687
+ display: flex;
688
+ gap: 8px;
689
+ flex-direction: row;
690
+ margin: 0;
691
+ }
692
+
693
+ .views .views-tabs {
694
+ flex: 0 0 auto;
695
+ }
696
+
697
+ .views .views-actions {
698
+ flex: 1 0 auto;
699
+ justify-content: flex-end;
700
+ gap: 10px;
701
+ }
702
+
703
+ .views .views-actions li {
704
+ display: flex;
705
+ align-items: center;
706
+ }
707
+
708
+ .views-container .download-btn {
709
+ padding: 6px 10px;
710
+ font-size: 11px;
711
+ }
712
+
713
+ .views .format.tab {
714
+ display: inline-block;
715
+ }
716
+
717
+ .views .format.tab a {
718
+ padding: 8px 0 6px 0;
719
+ border-radius: 0;
720
+ background: transparent;
721
+ color: #666;
722
+ text-decoration: none;
723
+ font-size: 12px;
724
+ font-weight: 600;
725
+ transition: all 0.2s ease;
726
+ display: inline-block;
727
+ border-bottom: 3px solid transparent;
728
+ cursor: pointer;
729
+ margin-right: 16px;
730
+ outline: none;
731
+ }
732
+
733
+ .views .format.tab a:hover {
734
+ color: #1a1a1a;
735
+ text-decoration: none;
736
+ }
737
+
738
+ .views .format.tab a:focus {
739
+ outline: none;
740
+ }
741
+
742
+ .views .format.tab.selected a,
743
+ .views .format.tab a.selected {
744
+ color: #2196F3 !important;
745
+ border-bottom: 3px solid #2196F3;
746
+ text-decoration: none !important;
747
+ }
748
+
749
+ .views .format.tab.selected a:hover,
750
+ .views .format.tab a.selected:hover {
751
+ color: #1976D2;
752
+ border-bottom-color: #1976D2;
753
+ }
754
+
755
+ .download-btn {
756
+ padding: 8px 12px;
757
+ border: 1px solid #e0e0e0;
758
+ border-radius: 6px;
759
+ background: #ffffff;
760
+ color: #1a1a1a;
761
+ cursor: pointer;
762
+ display: flex;
763
+ align-items: center;
764
+ gap: 6px;
765
+ font-size: 12px;
766
+ font-weight: 600;
767
+ transition: all 0.2s;
768
+ text-decoration: none;
769
+ }
770
+
771
+ .download-btn:hover {
772
+ background: #f8f8f8;
773
+ border-color: #d0d0d0;
774
+ }
775
+
776
+ .download-icon {
777
+ width: 14px;
778
+ height: 14px;
779
+ display: inline-block;
780
+ }
781
+
782
+ #message iframe.body {
783
+ flex: 1;
784
+ border: none;
785
+ background: #ffffff;
786
+ }
787
+
788
+ #message iframe.body[src*=".source"] {
789
+ background: #f5f5f5;
790
+ }
791
+
792
+ #noscript-overlay {
793
+ position: fixed;
794
+ top: 0;
795
+ left: 0;
796
+ right: 0;
797
+ bottom: 0;
798
+ background: white;
799
+ display: flex;
800
+ align-items: center;
801
+ justify-content: center;
802
+ z-index: 1000;
803
+ }
804
+
805
+ #noscript {
806
+ text-align: center;
807
+ font-size: 18px;
808
+ color: #333;
809
+ }
810
+
811
+ ::-webkit-scrollbar {
812
+ width: 8px;
813
+ }
814
+
815
+ ::-webkit-scrollbar-track {
816
+ background: #f0f0f0;
817
+ }
818
+
819
+ ::-webkit-scrollbar-thumb {
820
+ background: #d0d0d0;
821
+ border-radius: 4px;
822
+ }
823
+
824
+ ::-webkit-scrollbar-thumb:hover {
825
+ background: #b0b0b0;
826
+ }
827
+
828
+ /* Email Signature Info Button */
829
+ .signature-info-btn {
830
+ display: inline-flex;
831
+ align-items: center;
832
+ justify-content: center;
833
+ width: 24px;
834
+ height: 24px;
835
+ padding: 0;
836
+ border: none;
837
+ background: #e3f2fd;
838
+ color: #1976D2;
839
+ border-radius: 50%;
840
+ cursor: pointer;
841
+ font-size: 14px;
842
+ font-weight: 600;
843
+ transition: all 0.2s;
844
+ flex-shrink: 0;
845
+ margin-left: 8px;
846
+ }
847
+
848
+ .signature-info-btn:hover {
849
+ background: #bbdefb;
850
+ color: #1565c0;
851
+ transform: scale(1.05);
852
+ }
853
+
854
+ .signature-info-btn:active {
855
+ transform: scale(0.95);
856
+ }
857
+
858
+ .signature-info-btn svg {
859
+ width: 18px;
860
+ height: 18px;
861
+ }
862
+
863
+ /* Encryption/Signature Tooltip Styling */
864
+ .encryption-tooltip-content {
865
+ padding: 12px 16px;
866
+ font-size: 13px;
867
+ line-height: 1.6;
868
+ max-width: 400px;
869
+ }
870
+
871
+ .encryption-tooltip-content h3 {
872
+ font-size: 12px;
873
+ font-weight: 600;
874
+ text-transform: uppercase;
875
+ letter-spacing: 0.5px;
876
+ color: #5f5f5f;
877
+ margin: 0 0 8px 0;
878
+ padding-top: 8px;
879
+ }
880
+
881
+ .encryption-tooltip-content h3:first-child {
882
+ padding-top: 0;
883
+ margin-top: 0;
884
+ }
885
+
886
+ .encryption-info-item {
887
+ display: flex;
888
+ align-items: flex-start;
889
+ gap: 8px;
890
+ margin-bottom: 6px;
891
+ }
892
+
893
+ .encryption-info-item:last-child {
894
+ margin-bottom: 0;
895
+ }
896
+
897
+ .encryption-info-label {
898
+ font-weight: 500;
899
+ color: #333;
900
+ min-width: 100px;
901
+ }
902
+
903
+ .encryption-info-value {
904
+ color: #666;
905
+ flex-grow: 1;
906
+ word-break: break-all;
907
+ }
908
+
909
+ .encryption-copy-button {
910
+ display: inline-flex;
911
+ align-items: center;
912
+ gap: 4px;
913
+ padding: 6px 12px;
914
+ margin-top: 8px;
915
+ background: #f5f5f5;
916
+ border: 1px solid #e0e0e0;
917
+ border-radius: 4px;
918
+ cursor: pointer;
919
+ font-size: 12px;
920
+ font-weight: 500;
921
+ color: #333;
922
+ transition: all 0.2s;
923
+ }
924
+
925
+ .encryption-copy-button:hover {
926
+ background: #efefef;
927
+ border-color: #d0d0d0;
928
+ }
929
+
930
+ .encryption-copy-button:active {
931
+ transform: scale(0.98);
932
+ }
933
+
934
+ .encryption-copy-button svg {
935
+ width: 14px;
936
+ height: 14px;
937
+ }
938
+
939
+ .encryption-copy-button.copied {
940
+ background: #c8e6c9;
941
+ border-color: #4caf50;
942
+ color: #2e7d32;
943
+ }
944
+
945
+ .encryption-copy-button.copied svg {
946
+ stroke: #2e7d32;
947
+ }
948
+
949
+ .encryption-no-data {
950
+ color: #999;
951
+ font-size: 12px;
952
+ }
953
+
954
+ /* Tippy tooltip customization */
955
+ .tippy-box[data-theme~='light'] {
956
+ background-color: #ffffff;
957
+ border: 1px solid #e8eaed;
958
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
959
+ border-radius: 8px;
960
+ color: #1a1a1a;
961
+ }
962
+
963
+ .tippy-box[data-theme~='light'][data-placement^='top'] > .tippy-arrow::before {
964
+ border-top-color: #e8eaed;
965
+ }
966
+
967
+ .tippy-box[data-theme~='light'][data-placement^='bottom'] > .tippy-arrow::before {
968
+ border-bottom-color: #e8eaed;
969
+ }
970
+
971
+ .tippy-box[data-theme~='light'][data-placement^='left'] > .tippy-arrow::before {
972
+ border-left-color: #e8eaed;
973
+ }
974
+
975
+ .tippy-box[data-theme~='light'][data-placement^='right'] > .tippy-arrow::before {
976
+ border-right-color: #e8eaed;
977
+ }
978
+
979
+ .signature-tooltip-content {
980
+ padding: 12px 16px;
981
+ font-size: 13px;
982
+ line-height: 1.6;
983
+ max-width: 360px;
984
+ }
985
+
986
+ .signature-tooltip-content h3 {
987
+ font-size: 12px;
988
+ font-weight: 600;
989
+ text-transform: uppercase;
990
+ letter-spacing: 0.5px;
991
+ color: #5f5f5f;
992
+ margin: 0 0 8px 0;
993
+ padding-top: 8px;
994
+ }
995
+
996
+ .signature-tooltip-content h3:first-child {
997
+ padding-top: 0;
998
+ margin-top: 0;
999
+ }
1000
+
1001
+ .signature-tooltip-item {
1002
+ display: flex;
1003
+ align-items: center;
1004
+ gap: 8px;
1005
+ margin-bottom: 6px;
1006
+ }
1007
+
1008
+ .signature-tooltip-item:last-child {
1009
+ margin-bottom: 0;
1010
+ }
1011
+
1012
+ .signature-status-badge {
1013
+ display: inline-flex;
1014
+ align-items: center;
1015
+ gap: 4px;
1016
+ padding: 2px 8px;
1017
+ border-radius: 12px;
1018
+ font-size: 12px;
1019
+ font-weight: 500;
1020
+ white-space: nowrap;
1021
+ }
1022
+
1023
+ .signature-status-badge.pass {
1024
+ background: #c8e6c9;
1025
+ color: #2e7d32;
1026
+ }
1027
+
1028
+ .signature-status-badge.fail {
1029
+ background: #ffcdd2;
1030
+ color: #c62828;
1031
+ }
1032
+
1033
+ .signature-status-badge.neutral {
1034
+ background: #f5f5f5;
1035
+ color: #666;
1036
+ }
1037
+
1038
+ .signature-status-badge::before {
1039
+ display: inline-block;
1040
+ width: 6px;
1041
+ height: 6px;
1042
+ border-radius: 50%;
1043
+ background: currentColor;
1044
+ content: '';
1045
+ }
1046
+
1047
+ /* Syntax highlighting for HTML source */
1048
+ pre {
1049
+ margin: 0;
1050
+ padding: 16px;
1051
+ background: #ffffff;
1052
+ border: 1px solid #e8eaed;
1053
+ border-radius: 8px;
1054
+ overflow-x: auto;
1055
+ font-family: 'Courier New', 'Monaco', 'Ubuntu Mono', monospace;
1056
+ font-size: 12px;
1057
+ line-height: 1.5;
1058
+ }
1059
+
1060
+ code {
1061
+ background: transparent;
1062
+ padding: 0;
1063
+ font-family: inherit;
1064
+ font-size: inherit;
1065
+ color: #1a1a1a;
1066
+ }
1067
+
1068
+ /* Override highlight.js default styles for light theme compatibility */
1069
+ .hljs {
1070
+ background: #ffffff;
1071
+ color: #1a1a1a;
1072
+ }
1073
+
1074
+ .hljs-tag {
1075
+ color: #2196F3;
1076
+ }
1077
+
1078
+ .hljs-attr {
1079
+ color: #2196F3;
1080
+ }
1081
+
1082
+ .hljs-string {
1083
+ color: #34a853;
1084
+ }
1085
+
1086
+ .hljs-number {
1087
+ color: #ea4335;
1088
+ }
1089
+
1090
+ .hljs-literal {
1091
+ color: #ea4335;
1092
+ }
1093
+ </style>
1094
+ </head>
1095
+ <body>
1096
+ <noscript>
1097
+ <div id="noscript-overlay">
1098
+ <div id="noscript">
1099
+ MailCatcher NG requires JavaScript to be enabled.
1100
+ </div>
1101
+ </div>
1102
+ </noscript>
1103
+ <header>
1104
+ <div>
1105
+ <h1><a href="https://github.com/spaquet/mailcatcher" target="_blank">MailCatcher NG</a></h1>
1106
+ <div class="version-badge"><%= @version %></div>
1107
+ </div>
1108
+ <div class="header-controls">
1109
+ <div class="search-box">
1110
+ <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1111
+ <circle cx="11" cy="11" r="8"></circle>
1112
+ <path d="m21 21-4.35-4.35"></path>
1113
+ </svg>
1114
+ <input type="search" name="search" placeholder="Search messages..." incremental="true" />
1115
+ <button type="button" class="search-clear" id="searchClear" title="Clear search">
1116
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1117
+ <line x1="18" y1="6" x2="6" y2="18"></line>
1118
+ <line x1="6" y1="6" x2="18" y2="18"></line>
1119
+ </svg>
1120
+ </button>
1121
+ </div>
1122
+ <div class="attachment-filter">
1123
+ <select id="attachmentFilter" title="Filter by attachments">
1124
+ <option value="all">All</option>
1125
+ <option value="with">With attachments</option>
1126
+ <option value="without">Without attachments</option>
1127
+ </select>
1128
+ </div>
1129
+ <div class="header-info">
1130
+ <div class="status-badge" id="websocketStatus">
1131
+ <div class="indicator"></div>
1132
+ <span id="statusText">Connected</span>
1133
+ </div>
1134
+ <div class="email-count" id="emailCount">0 emails</div>
1135
+ <div class="action-buttons">
1136
+ <button class="btn" id="serverInfoBtn" title="View server information">
1137
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1138
+ <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
1139
+ <line x1="2" y1="17" x2="22" y2="17"></line>
1140
+ <polyline points="15 21 12 17 9 21"></polyline>
1141
+ </svg>
1142
+ Server
1143
+ </button>
1144
+ <button class="btn" id="clearBtn" title="Clear all messages">
1145
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1146
+ <polyline points="3 6 5 6 21 6"></polyline>
1147
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
1148
+ <line x1="10" y1="11" x2="10" y2="17"></line>
1149
+ <line x1="14" y1="11" x2="14" y2="17"></line>
1150
+ </svg>
1151
+ Clear
1152
+ </button>
1153
+ <% if MailCatcher.quittable? %>
1154
+ <button class="btn" id="quitBtn" title="Quit MailCatcher">
1155
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1156
+ <polyline points="23 6 13.46 15.89"></polyline>
1157
+ <polyline points="23 6 6.21 23"></polyline>
1158
+ <polyline points="1 1 21 21"></polyline>
1159
+ </svg>
1160
+ Quit
1161
+ </button>
1162
+ <% end %>
1163
+ </div>
1164
+ </div>
1165
+ </div>
1166
+ </header>
1167
+ <main>
1168
+ <nav id="messages">
1169
+ <table>
1170
+ <thead>
1171
+ <tr>
1172
+ <th class="col-attachments"></th>
1173
+ <th class="col-bimi"></th>
1174
+ <th>From</th>
1175
+ <th>To</th>
1176
+ <th>Subject</th>
1177
+ <th>Received</th>
1178
+ <th>Size</th>
1179
+ </tr>
1180
+ </thead>
1181
+ <tbody></tbody>
1182
+ </table>
1183
+ </nav>
1184
+ <div id="resizer"></div>
1185
+ <article id="message">
1186
+ <header>
1187
+ <div class="views-container">
1188
+ <nav class="views">
1189
+ <ul class="views-tabs">
1190
+ <li class="format tab html selected" data-message-format="html"><a href="#">HTML</a></li>
1191
+ <li class="format tab plain" data-message-format="plain"><a href="#">Plain Text</a></li>
1192
+ <li class="format tab source" data-message-format="source"><a href="#">Source</a></li>
1193
+ </ul>
1194
+ <ul class="views-actions">
1195
+ <li><button class="signature-info-btn" id="encryptionInfoBtn" title="Email encryption and signature information">
1196
+ <svg data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
1197
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"></path>
1198
+ </svg>
1199
+ </button></li>
1200
+ <li><button class="signature-info-btn" id="signatureInfoBtn" title="Email signature verification (DMARC, DKIM, SPF)">
1201
+ <svg data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
1202
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z"></path>
1203
+ </svg>
1204
+ </button></li>
1205
+ <li><a href="#" class="download-btn" data-message-format="html">
1206
+ <svg class="download-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1207
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
1208
+ <polyline points="7 10 12 15 17 10"></polyline>
1209
+ <line x1="12" y1="15" x2="12" y2="3"></line>
1210
+ </svg>
1211
+ Download
1212
+ </a></li>
1213
+ </ul>
1214
+ </nav>
1215
+ </div>
1216
+ <div class="metadata">
1217
+ <div class="metadata-column">
1218
+ <div class="metadata-item">
1219
+ <dt class="created_at">Received</dt>
1220
+ <dd class="created_at"></dd>
1221
+ </div>
1222
+ <div class="metadata-item">
1223
+ <dt class="from">From</dt>
1224
+ <dd class="from"></dd>
1225
+ </div>
1226
+ <div class="metadata-item">
1227
+ <dt class="to">To</dt>
1228
+ <dd class="to"></dd>
1229
+ </div>
1230
+ <div class="metadata-item">
1231
+ <dt class="subject">Subject</dt>
1232
+ <dd class="subject"></dd>
1233
+ </div>
1234
+ </div>
1235
+ </div>
1236
+ <div class="attachments-column">
1237
+ <div class="attachments-header">Attachments</div>
1238
+ <ul class="attachments-list"></ul>
1239
+ </div>
1240
+ </header>
1241
+ <iframe class="body"></iframe>
1242
+ </article>
1243
+ </main>
1244
+ <script>
1245
+ // Update email count display
1246
+ function updateEmailCount() {
1247
+ const count = document.querySelectorAll('#messages tbody tr').length;
1248
+ document.getElementById('emailCount').textContent = count === 1 ? '1 email' : count + ' emails';
1249
+ }
1250
+
1251
+ // Setup additional UI handlers
1252
+ document.addEventListener('DOMContentLoaded', function() {
1253
+ // Patch the original refresh to update email count
1254
+ const originalRefresh = window.MailCatcher?.refresh;
1255
+ if (originalRefresh) {
1256
+ window.MailCatcher.refresh = function() {
1257
+ originalRefresh.call(this);
1258
+ updateEmailCount();
1259
+ };
1260
+ }
1261
+
1262
+ // Setup resizer drag functionality
1263
+ const resizer = document.getElementById('resizer');
1264
+ const messagesSection = document.getElementById('messages');
1265
+ let isResizing = false;
1266
+
1267
+ if (resizer && messagesSection) {
1268
+ resizer.addEventListener('mousedown', function(e) {
1269
+ e.preventDefault();
1270
+ isResizing = true;
1271
+ const startY = e.clientY;
1272
+ const startHeight = messagesSection.offsetHeight;
1273
+
1274
+ const handleMouseMove = (e) => {
1275
+ if (!isResizing) return;
1276
+ const delta = e.clientY - startY;
1277
+ const newHeight = Math.max(150, startHeight + delta);
1278
+ messagesSection.style.flex = `0 0 ${newHeight}px`;
1279
+ localStorage.setItem('mailcatcherSeparatorHeight', newHeight);
1280
+ };
1281
+
1282
+ const handleMouseUp = () => {
1283
+ isResizing = false;
1284
+ document.removeEventListener('mousemove', handleMouseMove);
1285
+ document.removeEventListener('mouseup', handleMouseUp);
1286
+ };
1287
+
1288
+ document.addEventListener('mousemove', handleMouseMove);
1289
+ document.addEventListener('mouseup', handleMouseUp);
1290
+ });
1291
+ }
1292
+
1293
+ // Restore saved resizer position
1294
+ const savedHeight = localStorage.getItem('mailcatcherSeparatorHeight');
1295
+ if (savedHeight && messagesSection) {
1296
+ messagesSection.style.flex = `0 0 ${savedHeight}px`;
1297
+ }
1298
+
1299
+ // Setup button handlers
1300
+ document.getElementById('serverInfoBtn').addEventListener('click', function(e) {
1301
+ e.preventDefault();
1302
+ window.location.href = new URL('server-info', document.baseURI).toString();
1303
+ });
1304
+
1305
+ document.getElementById('clearBtn').addEventListener('click', function(e) {
1306
+ e.preventDefault();
1307
+ if (window.MailCatcher && document.querySelectorAll('#messages tbody tr').length > 0) {
1308
+ const confirmText = 'You will lose all your received messages.\n\nAre you sure you want to clear all messages?';
1309
+ if (confirm(confirmText)) {
1310
+ window.MailCatcher.clearMessages();
1311
+ // Also send DELETE request
1312
+ fetch(new URL('messages', document.baseURI).toString(), { method: 'DELETE' });
1313
+ }
1314
+ }
1315
+ });
1316
+
1317
+ const quitBtn = document.getElementById('quitBtn');
1318
+ if (quitBtn) {
1319
+ quitBtn.addEventListener('click', function(e) {
1320
+ e.preventDefault();
1321
+ const confirmText = 'You will lose all your received messages.\n\nAre you sure you want to quit?';
1322
+ if (confirm(confirmText)) {
1323
+ fetch(new URL('', document.baseURI).toString(), { method: 'DELETE' });
1324
+ }
1325
+ });
1326
+ }
1327
+
1328
+ // Monitor updates for email count
1329
+ const observer = new MutationObserver(() => updateEmailCount());
1330
+ const tbody = document.querySelector('#messages tbody');
1331
+ if (tbody) {
1332
+ observer.observe(tbody, { childList: true });
1333
+ }
1334
+
1335
+ // Handle download button
1336
+ document.querySelector('.download-btn').addEventListener('click', function(e) {
1337
+ e.preventDefault();
1338
+ if (window.MailCatcher) {
1339
+ const id = window.MailCatcher.selectedMessage();
1340
+ if (id) {
1341
+ window.location.href = `messages/${id}.eml`;
1342
+ }
1343
+ }
1344
+ });
1345
+
1346
+ // Setup email signature info tooltip
1347
+ const signatureInfoBtn = document.getElementById('signatureInfoBtn');
1348
+ let signatureTooltip = null;
1349
+
1350
+ function getStatusBadgeClass(status) {
1351
+ if (!status) return 'neutral';
1352
+ return status === 'pass' ? 'pass' : status === 'fail' ? 'fail' : 'neutral';
1353
+ }
1354
+
1355
+ function getStatusLabel(status) {
1356
+ if (!status) return 'Not checked';
1357
+ return status.charAt(0).toUpperCase() + status.slice(1);
1358
+ }
1359
+
1360
+ function generateSignatureContent(authResults) {
1361
+ const dmarc = authResults?.dmarc;
1362
+ const dkim = authResults?.dkim;
1363
+ const spf = authResults?.spf;
1364
+
1365
+ // Check if any auth data exists
1366
+ const hasAuthData = dmarc || dkim || spf;
1367
+
1368
+ if (!hasAuthData) {
1369
+ return `
1370
+ <div class="signature-tooltip-content">
1371
+ <p style="color: #999; font-size: 12px;">No authentication headers found for this email.</p>
1372
+ </div>
1373
+ `;
1374
+ }
1375
+
1376
+ return `
1377
+ <div class="signature-tooltip-content">
1378
+ ${dmarc ? `
1379
+ <h3>DMARC</h3>
1380
+ <div class="signature-tooltip-item">
1381
+ <span class="signature-status-badge ${getStatusBadgeClass(dmarc)}">
1382
+ ${getStatusLabel(dmarc)}
1383
+ </span>
1384
+ </div>
1385
+ ` : ''}
1386
+ ${dkim ? `
1387
+ <h3>DKIM</h3>
1388
+ <div class="signature-tooltip-item">
1389
+ <span class="signature-status-badge ${getStatusBadgeClass(dkim)}">
1390
+ ${getStatusLabel(dkim)}
1391
+ </span>
1392
+ </div>
1393
+ ` : ''}
1394
+ ${spf ? `
1395
+ <h3>SPF</h3>
1396
+ <div class="signature-tooltip-item">
1397
+ <span class="signature-status-badge ${getStatusBadgeClass(spf)}">
1398
+ ${getStatusLabel(spf)}
1399
+ </span>
1400
+ </div>
1401
+ ` : ''}
1402
+ </div>
1403
+ `;
1404
+ }
1405
+
1406
+ if (signatureInfoBtn) {
1407
+ signatureInfoBtn.addEventListener('click', function(e) {
1408
+ e.preventDefault();
1409
+ if (window.MailCatcher) {
1410
+ const messageId = window.MailCatcher.selectedMessage();
1411
+ if (!messageId) return;
1412
+
1413
+ // Destroy existing tooltip if any
1414
+ if (signatureTooltip) {
1415
+ signatureTooltip.destroy();
1416
+ }
1417
+
1418
+ // Fetch message data
1419
+ fetch(new URL(`messages/${messageId}.json`, document.baseURI).toString())
1420
+ .then(response => response.json())
1421
+ .then(data => {
1422
+ const authResults = data.authentication_results || {};
1423
+ const content = generateSignatureContent(authResults);
1424
+
1425
+ // Create tooltip with content
1426
+ signatureTooltip = tippy(signatureInfoBtn, {
1427
+ content: content,
1428
+ allowHTML: true,
1429
+ theme: 'light',
1430
+ placement: 'bottom-start',
1431
+ interactive: true,
1432
+ duration: [200, 150],
1433
+ arrow: true,
1434
+ trigger: 'manual',
1435
+ maxWidth: 360,
1436
+ onClickOutside: (instance) => {
1437
+ instance.hide();
1438
+ },
1439
+ });
1440
+
1441
+ // Show tooltip
1442
+ signatureTooltip.show();
1443
+ })
1444
+ .catch(error => {
1445
+ console.error('Error fetching signature data:', error);
1446
+ const errorContent = `
1447
+ <div class="signature-tooltip-content">
1448
+ <p style="color: #999; font-size: 12px;">Error loading signature data.</p>
1449
+ </div>
1450
+ `;
1451
+
1452
+ signatureTooltip = tippy(signatureInfoBtn, {
1453
+ content: errorContent,
1454
+ allowHTML: true,
1455
+ theme: 'light',
1456
+ placement: 'bottom-start',
1457
+ interactive: true,
1458
+ duration: [200, 150],
1459
+ arrow: true,
1460
+ trigger: 'manual',
1461
+ onClickOutside: (instance) => {
1462
+ instance.hide();
1463
+ },
1464
+ });
1465
+
1466
+ signatureTooltip.show();
1467
+ });
1468
+ }
1469
+ });
1470
+
1471
+ // Close tooltip when clicking elsewhere
1472
+ document.addEventListener('click', function(e) {
1473
+ // Check if this click is on the button
1474
+ if (signatureInfoBtn && signatureInfoBtn.contains(e.target)) return;
1475
+
1476
+ // Check if this click is on a tippy-box
1477
+ if (e.target.closest('.tippy-box')) return;
1478
+
1479
+ // Click is outside everything, hide tooltip
1480
+ if (signatureTooltip) {
1481
+ signatureTooltip.hide();
1482
+ }
1483
+
1484
+ // Also ensure any visible tippy-box is hidden
1485
+ const visibleBoxes = document.querySelectorAll('.tippy-box[data-state="visible"]');
1486
+ visibleBoxes.forEach(box => {
1487
+ box.style.visibility = 'hidden';
1488
+ box.style.pointerEvents = 'none';
1489
+ });
1490
+ });
1491
+ }
1492
+
1493
+ // Setup encryption/signature info tooltip
1494
+ const encryptionInfoBtn = document.getElementById('encryptionInfoBtn');
1495
+ let encryptionTooltip = null;
1496
+
1497
+ function generateEncryptionContent(encryptionData) {
1498
+ const smime = encryptionData?.smime;
1499
+ const pgp = encryptionData?.pgp;
1500
+
1501
+ // Check if any encryption data exists
1502
+ const hasEncryptionData = smime || pgp;
1503
+
1504
+ if (!hasEncryptionData) {
1505
+ return `
1506
+ <div class="encryption-tooltip-content">
1507
+ <p class="encryption-no-data">No encryption or signature information found for this email.</p>
1508
+ </div>
1509
+ `;
1510
+ }
1511
+
1512
+ let content = '<div class="encryption-tooltip-content">';
1513
+
1514
+ if (smime) {
1515
+ content += `
1516
+ <h3>S/MIME</h3>
1517
+ ${smime.certificate ? `
1518
+ <div class="encryption-info-item">
1519
+ <span class="encryption-info-label">Certificate:</span>
1520
+ <span class="encryption-info-value">${escapeHtml(smime.certificate.substring(0, 40))}...</span>
1521
+ </div>
1522
+ <button class="encryption-copy-button" data-copy-type="smime-cert" data-value="${escapeHtml(smime.certificate)}">
1523
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1524
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z"></path>
1525
+ </svg>
1526
+ Copy Certificate
1527
+ </button>
1528
+ ` : ''}
1529
+ ${smime.signature ? `
1530
+ <div class="encryption-info-item">
1531
+ <span class="encryption-info-label">Signature:</span>
1532
+ <span class="encryption-info-value">${escapeHtml(smime.signature.substring(0, 40))}...</span>
1533
+ </div>
1534
+ <button class="encryption-copy-button" data-copy-type="smime-sig" data-value="${escapeHtml(smime.signature)}">
1535
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1536
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z"></path>
1537
+ </svg>
1538
+ Copy Signature
1539
+ </button>
1540
+ ` : ''}
1541
+ `;
1542
+ }
1543
+
1544
+ if (pgp) {
1545
+ content += `
1546
+ <h3>OpenPGP</h3>
1547
+ ${pgp.key ? `
1548
+ <div class="encryption-info-item">
1549
+ <span class="encryption-info-label">Key:</span>
1550
+ <span class="encryption-info-value">${escapeHtml(pgp.key.substring(0, 40))}...</span>
1551
+ </div>
1552
+ <button class="encryption-copy-button" data-copy-type="pgp-key" data-value="${escapeHtml(pgp.key)}">
1553
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1554
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z"></path>
1555
+ </svg>
1556
+ Copy Key
1557
+ </button>
1558
+ ` : ''}
1559
+ ${pgp.signature ? `
1560
+ <div class="encryption-info-item">
1561
+ <span class="encryption-info-label">Signature:</span>
1562
+ <span class="encryption-info-value">${escapeHtml(pgp.signature.substring(0, 40))}...</span>
1563
+ </div>
1564
+ <button class="encryption-copy-button" data-copy-type="pgp-sig" data-value="${escapeHtml(pgp.signature)}">
1565
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1566
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5A3.375 3.375 0 0 0 6.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-1.5a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 0 0-9-9Z"></path>
1567
+ </svg>
1568
+ Copy Signature
1569
+ </button>
1570
+ ` : ''}
1571
+ `;
1572
+ }
1573
+
1574
+ content += '</div>';
1575
+ return content;
1576
+ }
1577
+
1578
+ function escapeHtml(text) {
1579
+ const map = {
1580
+ '&': '&amp;',
1581
+ '<': '&lt;',
1582
+ '>': '&gt;',
1583
+ '"': '&quot;',
1584
+ "'": '&#039;'
1585
+ };
1586
+ return text.replace(/[&<>"']/g, m => map[m]);
1587
+ }
1588
+
1589
+ if (encryptionInfoBtn) {
1590
+ encryptionInfoBtn.addEventListener('click', function(e) {
1591
+ e.preventDefault();
1592
+ if (window.MailCatcher) {
1593
+ const messageId = window.MailCatcher.selectedMessage();
1594
+ if (!messageId) return;
1595
+
1596
+ // Destroy existing tooltip if any
1597
+ if (encryptionTooltip) {
1598
+ encryptionTooltip.destroy();
1599
+ }
1600
+
1601
+ // Fetch message data
1602
+ fetch(new URL(`messages/${messageId}.json`, document.baseURI).toString())
1603
+ .then(response => response.json())
1604
+ .then(data => {
1605
+ const encryptionData = data.encryption_data || {};
1606
+ const content = generateEncryptionContent(encryptionData);
1607
+
1608
+ // Create tooltip with content
1609
+ encryptionTooltip = tippy(encryptionInfoBtn, {
1610
+ content: content,
1611
+ allowHTML: true,
1612
+ theme: 'light',
1613
+ placement: 'bottom-start',
1614
+ interactive: true,
1615
+ duration: [200, 150],
1616
+ arrow: true,
1617
+ trigger: 'manual',
1618
+ maxWidth: 400,
1619
+ onClickOutside: (instance) => {
1620
+ instance.hide();
1621
+ },
1622
+ });
1623
+
1624
+ // Show tooltip
1625
+ encryptionTooltip.show();
1626
+
1627
+ // Setup copy button handlers
1628
+ const copyButtons = document.querySelectorAll('.encryption-copy-button');
1629
+ copyButtons.forEach(btn => {
1630
+ btn.addEventListener('click', function(e) {
1631
+ e.preventDefault();
1632
+ e.stopPropagation();
1633
+
1634
+ const value = this.getAttribute('data-value');
1635
+ const copyType = this.getAttribute('data-copy-type');
1636
+
1637
+ // Copy to clipboard
1638
+ navigator.clipboard.writeText(value).then(() => {
1639
+ // Show success state
1640
+ const originalHTML = this.innerHTML;
1641
+ const originalClass = this.className;
1642
+
1643
+ this.classList.add('copied');
1644
+ this.innerHTML = `
1645
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1646
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75"></path>
1647
+ </svg>
1648
+ Copied!
1649
+ `;
1650
+
1651
+ // Reset after 3 seconds
1652
+ setTimeout(() => {
1653
+ this.className = originalClass;
1654
+ this.innerHTML = originalHTML;
1655
+ }, 3000);
1656
+ }).catch(err => {
1657
+ console.error('Failed to copy to clipboard:', err);
1658
+ });
1659
+ });
1660
+ });
1661
+ })
1662
+ .catch(error => {
1663
+ console.error('Error fetching encryption data:', error);
1664
+ const errorContent = `
1665
+ <div class="encryption-tooltip-content">
1666
+ <p class="encryption-no-data">Error loading encryption data.</p>
1667
+ </div>
1668
+ `;
1669
+
1670
+ encryptionTooltip = tippy(encryptionInfoBtn, {
1671
+ content: errorContent,
1672
+ allowHTML: true,
1673
+ theme: 'light',
1674
+ placement: 'bottom-start',
1675
+ interactive: true,
1676
+ duration: [200, 150],
1677
+ arrow: true,
1678
+ trigger: 'manual',
1679
+ onClickOutside: (instance) => {
1680
+ instance.hide();
1681
+ },
1682
+ });
1683
+
1684
+ encryptionTooltip.show();
1685
+ });
1686
+ }
1687
+ });
1688
+
1689
+ // Close tooltip when clicking elsewhere
1690
+ document.addEventListener('click', function(e) {
1691
+ // Check if this click is on the button
1692
+ if (encryptionInfoBtn && encryptionInfoBtn.contains(e.target)) return;
1693
+
1694
+ // Check if this click is on a tippy-box
1695
+ if (e.target.closest('.tippy-box')) return;
1696
+
1697
+ // Click is outside everything, hide tooltip
1698
+ if (encryptionTooltip) {
1699
+ encryptionTooltip.hide();
1700
+ }
1701
+ });
1702
+ }
1703
+ });
1704
+ </script>
1705
+ </body>
1706
+ </html>