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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +235 -0
- data/bin/catchmail +72 -0
- data/bin/mailcatcher +6 -0
- data/lib/mail_catcher/bus.rb +7 -0
- data/lib/mail_catcher/mail.rb +419 -0
- data/lib/mail_catcher/smtp.rb +68 -0
- data/lib/mail_catcher/version.rb +5 -0
- data/lib/mail_catcher/web/application.rb +247 -0
- data/lib/mail_catcher/web.rb +29 -0
- data/lib/mail_catcher.rb +257 -0
- data/lib/mailcatcher.rb +5 -0
- data/public/assets/mailcatcher.js +4 -0
- data/public/favicon.ico +0 -0
- data/views/404.erb +117 -0
- data/views/index.erb +1706 -0
- data/views/server_info.erb +236 -0
- data/views/websocket_test.erb +263 -0
- metadata +382 -0
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
|
+
'&': '&',
|
|
1581
|
+
'<': '<',
|
|
1582
|
+
'>': '>',
|
|
1583
|
+
'"': '"',
|
|
1584
|
+
"'": '''
|
|
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>
|