mailcatcher-ng 1.3.0 → 1.3.1
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 +4 -4
- data/lib/mail_catcher/version.rb +1 -1
- data/public/assets/mailcatcher-ui.js +536 -0
- data/public/assets/mailcatcher.css +1303 -0
- data/public/assets/mailcatcher.js +1 -1
- data/views/index.erb +4 -1814
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71c31256fbd4bdc76749770fe321db1351839f23fd01d1ef2ab6f21382da774b
|
|
4
|
+
data.tar.gz: 7f77c22a8dd2e3b7cea54c6ec415af7f408ad39588e48772cffaec625dd8fd15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e47f2938af045fd8da6b1e7eacb1a0552a5076bd5415c1079db271335271fc1299c3c47e3dfc78b9eaf1383699f339c1b7f674ff3169bdf65f270666c819255
|
|
7
|
+
data.tar.gz: c49f21a836aae6a35f84568997ddc29f4c619b3cebead68421a882b4ab3ff5ae4685f39e7a69a0473840d83831f88b55b01b4cbe0af5399981f04663319e65f5
|
data/lib/mail_catcher/version.rb
CHANGED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
// Utility functions for MailCatcher UI
|
|
2
|
+
window.MailCatcherUI = window.MailCatcherUI || {};
|
|
3
|
+
|
|
4
|
+
// HTML escape utility
|
|
5
|
+
window.MailCatcherUI.escapeHtml = function(text) {
|
|
6
|
+
const map = {
|
|
7
|
+
'&': '&',
|
|
8
|
+
'<': '<',
|
|
9
|
+
'>': '>',
|
|
10
|
+
'"': '"',
|
|
11
|
+
"'": '''
|
|
12
|
+
};
|
|
13
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
14
|
+
};
|
|
15
|
+
// Message list resizer functionality
|
|
16
|
+
window.MailCatcherUI = window.MailCatcherUI || {};
|
|
17
|
+
|
|
18
|
+
window.MailCatcherUI.initializeResizer = function() {
|
|
19
|
+
const resizer = document.getElementById('resizer');
|
|
20
|
+
const messagesSection = document.getElementById('messages');
|
|
21
|
+
let isResizing = false;
|
|
22
|
+
|
|
23
|
+
if (resizer && messagesSection) {
|
|
24
|
+
resizer.addEventListener('mousedown', function(e) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
isResizing = true;
|
|
27
|
+
const startY = e.clientY;
|
|
28
|
+
const startHeight = messagesSection.offsetHeight;
|
|
29
|
+
|
|
30
|
+
const handleMouseMove = (e) => {
|
|
31
|
+
if (!isResizing) return;
|
|
32
|
+
const delta = e.clientY - startY;
|
|
33
|
+
const newHeight = Math.max(150, startHeight + delta);
|
|
34
|
+
messagesSection.style.flex = `0 0 ${newHeight}px`;
|
|
35
|
+
localStorage.setItem('mailcatcherSeparatorHeight', newHeight);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleMouseUp = () => {
|
|
39
|
+
isResizing = false;
|
|
40
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
41
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
45
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Restore saved resizer position
|
|
50
|
+
const savedHeight = localStorage.getItem('mailcatcherSeparatorHeight');
|
|
51
|
+
if (savedHeight && messagesSection) {
|
|
52
|
+
messagesSection.style.flex = `0 0 ${savedHeight}px`;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
// UI event handlers (buttons, count, etc.)
|
|
56
|
+
window.MailCatcherUI = window.MailCatcherUI || {};
|
|
57
|
+
|
|
58
|
+
window.MailCatcherUI.initializeUIHandlers = function() {
|
|
59
|
+
// Update email count display
|
|
60
|
+
function updateEmailCount() {
|
|
61
|
+
const count = document.querySelectorAll('#messages tbody tr').length;
|
|
62
|
+
document.getElementById('emailCount').textContent = count === 1 ? '1 email' : count + ' emails';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Setup button handlers
|
|
66
|
+
const serverInfoBtn = document.getElementById('serverInfoBtn');
|
|
67
|
+
if (serverInfoBtn) {
|
|
68
|
+
serverInfoBtn.addEventListener('click', function(e) {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
window.location.href = new URL('server-info', document.baseURI).toString();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const clearBtn = document.getElementById('clearBtn');
|
|
75
|
+
if (clearBtn) {
|
|
76
|
+
clearBtn.addEventListener('click', function(e) {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
if (window.MailCatcher && document.querySelectorAll('#messages tbody tr').length > 0) {
|
|
79
|
+
const confirmText = 'You will lose all your received messages.\n\nAre you sure you want to clear all messages?';
|
|
80
|
+
if (confirm(confirmText)) {
|
|
81
|
+
window.MailCatcher.clearMessages();
|
|
82
|
+
// Also send DELETE request
|
|
83
|
+
fetch(new URL('messages', document.baseURI).toString(), { method: 'DELETE' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const quitBtn = document.getElementById('quitBtn');
|
|
90
|
+
if (quitBtn) {
|
|
91
|
+
quitBtn.addEventListener('click', function(e) {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
const confirmText = 'You will lose all your received messages.\n\nAre you sure you want to quit?';
|
|
94
|
+
if (confirm(confirmText)) {
|
|
95
|
+
fetch(new URL('', document.baseURI).toString(), { method: 'DELETE' });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Monitor updates for email count
|
|
101
|
+
const observer = new MutationObserver(() => updateEmailCount());
|
|
102
|
+
const tbody = document.querySelector('#messages tbody');
|
|
103
|
+
if (tbody) {
|
|
104
|
+
observer.observe(tbody, { childList: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle download button
|
|
108
|
+
const downloadBtn = document.querySelector('.download-btn');
|
|
109
|
+
if (downloadBtn) {
|
|
110
|
+
downloadBtn.addEventListener('click', function(e) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
if (window.MailCatcher) {
|
|
113
|
+
const id = window.MailCatcher.selectedMessage();
|
|
114
|
+
if (id) {
|
|
115
|
+
window.location.href = `messages/${id}.eml`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle copy source button
|
|
122
|
+
const copySourceBtn = document.getElementById('copySourceBtn');
|
|
123
|
+
if (copySourceBtn) {
|
|
124
|
+
copySourceBtn.addEventListener('click', function(e) {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
if (window.MailCatcher) {
|
|
128
|
+
const id = window.MailCatcher.selectedMessage();
|
|
129
|
+
if (id) {
|
|
130
|
+
// Fetch the raw email source
|
|
131
|
+
fetch(`messages/${id}.source`)
|
|
132
|
+
.then(response => response.text())
|
|
133
|
+
.then(sourceText => {
|
|
134
|
+
// Copy to clipboard
|
|
135
|
+
navigator.clipboard.writeText(sourceText)
|
|
136
|
+
.then(() => {
|
|
137
|
+
// Show checkmark icon
|
|
138
|
+
const copyIcon = copySourceBtn.querySelector('.copy-icon');
|
|
139
|
+
const checkmarkIcon = copySourceBtn.querySelector('.checkmark-icon');
|
|
140
|
+
|
|
141
|
+
if (copyIcon && checkmarkIcon) {
|
|
142
|
+
copyIcon.style.display = 'none';
|
|
143
|
+
checkmarkIcon.style.display = 'block';
|
|
144
|
+
|
|
145
|
+
// Reset after 3 seconds
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
copyIcon.style.display = 'block';
|
|
148
|
+
checkmarkIcon.style.display = 'none';
|
|
149
|
+
}, 3000);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log('Email source copied to clipboard');
|
|
153
|
+
})
|
|
154
|
+
.catch(err => {
|
|
155
|
+
console.error('Failed to copy to clipboard:', err);
|
|
156
|
+
alert('Failed to copy email source to clipboard');
|
|
157
|
+
});
|
|
158
|
+
})
|
|
159
|
+
.catch(err => {
|
|
160
|
+
console.error('Failed to fetch email source:', err);
|
|
161
|
+
alert('Failed to load email source');
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Initial count update
|
|
169
|
+
updateEmailCount();
|
|
170
|
+
};
|
|
171
|
+
// Email signature and encryption tooltips
|
|
172
|
+
window.MailCatcherUI = window.MailCatcherUI || {};
|
|
173
|
+
|
|
174
|
+
window.MailCatcherUI.initializeSignatureTooltip = function() {
|
|
175
|
+
const signatureInfoBtn = document.getElementById('signatureInfoBtn');
|
|
176
|
+
let signatureTooltip = null;
|
|
177
|
+
|
|
178
|
+
function getStatusBadgeClass(status) {
|
|
179
|
+
if (!status) return 'neutral';
|
|
180
|
+
return status === 'pass' ? 'pass' : status === 'fail' ? 'fail' : 'neutral';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getStatusLabel(status) {
|
|
184
|
+
if (!status) return 'Not checked';
|
|
185
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function generateSignatureContent(authResults) {
|
|
189
|
+
const dmarc = authResults?.dmarc;
|
|
190
|
+
const dkim = authResults?.dkim;
|
|
191
|
+
const spf = authResults?.spf;
|
|
192
|
+
|
|
193
|
+
// Check if any auth data exists
|
|
194
|
+
const hasAuthData = dmarc || dkim || spf;
|
|
195
|
+
|
|
196
|
+
if (!hasAuthData) {
|
|
197
|
+
return `
|
|
198
|
+
<div class="signature-tooltip-content">
|
|
199
|
+
<p style="color: #999; font-size: 12px;">No authentication headers found for this email.</p>
|
|
200
|
+
</div>
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return `
|
|
205
|
+
<div class="signature-tooltip-content">
|
|
206
|
+
${dmarc ? `
|
|
207
|
+
<h3>DMARC</h3>
|
|
208
|
+
<div class="signature-tooltip-item">
|
|
209
|
+
<span class="signature-status-badge ${getStatusBadgeClass(dmarc)}">
|
|
210
|
+
${getStatusLabel(dmarc)}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
` : ''}
|
|
214
|
+
${dkim ? `
|
|
215
|
+
<h3>DKIM</h3>
|
|
216
|
+
<div class="signature-tooltip-item">
|
|
217
|
+
<span class="signature-status-badge ${getStatusBadgeClass(dkim)}">
|
|
218
|
+
${getStatusLabel(dkim)}
|
|
219
|
+
</span>
|
|
220
|
+
</div>
|
|
221
|
+
` : ''}
|
|
222
|
+
${spf ? `
|
|
223
|
+
<h3>SPF</h3>
|
|
224
|
+
<div class="signature-tooltip-item">
|
|
225
|
+
<span class="signature-status-badge ${getStatusBadgeClass(spf)}">
|
|
226
|
+
${getStatusLabel(spf)}
|
|
227
|
+
</span>
|
|
228
|
+
</div>
|
|
229
|
+
` : ''}
|
|
230
|
+
</div>
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (signatureInfoBtn) {
|
|
235
|
+
signatureInfoBtn.addEventListener('click', function(e) {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
if (window.MailCatcher) {
|
|
238
|
+
const messageId = window.MailCatcher.selectedMessage();
|
|
239
|
+
if (!messageId) return;
|
|
240
|
+
|
|
241
|
+
// Destroy existing tooltip if any
|
|
242
|
+
if (signatureTooltip) {
|
|
243
|
+
signatureTooltip.destroy();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Fetch message data
|
|
247
|
+
fetch(new URL(`messages/${messageId}.json`, document.baseURI).toString())
|
|
248
|
+
.then(response => response.json())
|
|
249
|
+
.then(data => {
|
|
250
|
+
const authResults = data.authentication_results || {};
|
|
251
|
+
const content = generateSignatureContent(authResults);
|
|
252
|
+
|
|
253
|
+
// Create tooltip with content
|
|
254
|
+
signatureTooltip = tippy(signatureInfoBtn, {
|
|
255
|
+
content: content,
|
|
256
|
+
allowHTML: true,
|
|
257
|
+
theme: 'light',
|
|
258
|
+
placement: 'bottom-start',
|
|
259
|
+
interactive: true,
|
|
260
|
+
duration: [200, 150],
|
|
261
|
+
arrow: true,
|
|
262
|
+
trigger: 'manual',
|
|
263
|
+
maxWidth: 360,
|
|
264
|
+
onClickOutside: (instance) => {
|
|
265
|
+
instance.hide();
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Show tooltip
|
|
270
|
+
signatureTooltip.show();
|
|
271
|
+
})
|
|
272
|
+
.catch(error => {
|
|
273
|
+
console.error('Error fetching signature data:', error);
|
|
274
|
+
const errorContent = `
|
|
275
|
+
<div class="signature-tooltip-content">
|
|
276
|
+
<p style="color: #999; font-size: 12px;">Error loading signature data.</p>
|
|
277
|
+
</div>
|
|
278
|
+
`;
|
|
279
|
+
|
|
280
|
+
signatureTooltip = tippy(signatureInfoBtn, {
|
|
281
|
+
content: errorContent,
|
|
282
|
+
allowHTML: true,
|
|
283
|
+
theme: 'light',
|
|
284
|
+
placement: 'bottom-start',
|
|
285
|
+
interactive: true,
|
|
286
|
+
duration: [200, 150],
|
|
287
|
+
arrow: true,
|
|
288
|
+
trigger: 'manual',
|
|
289
|
+
onClickOutside: (instance) => {
|
|
290
|
+
instance.hide();
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
signatureTooltip.show();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Close tooltip when clicking elsewhere
|
|
300
|
+
document.addEventListener('click', function(e) {
|
|
301
|
+
// Check if this click is on the button
|
|
302
|
+
if (signatureInfoBtn && signatureInfoBtn.contains(e.target)) return;
|
|
303
|
+
|
|
304
|
+
// Check if this click is on a tippy-box
|
|
305
|
+
if (e.target.closest('.tippy-box')) return;
|
|
306
|
+
|
|
307
|
+
// Click is outside everything, hide tooltip
|
|
308
|
+
if (signatureTooltip) {
|
|
309
|
+
signatureTooltip.hide();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Also ensure any visible tippy-box is hidden
|
|
313
|
+
const visibleBoxes = document.querySelectorAll('.tippy-box[data-state="visible"]');
|
|
314
|
+
visibleBoxes.forEach(box => {
|
|
315
|
+
box.style.visibility = 'hidden';
|
|
316
|
+
box.style.pointerEvents = 'none';
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
window.MailCatcherUI.initializeEncryptionTooltip = function() {
|
|
323
|
+
const encryptionInfoBtn = document.getElementById('encryptionInfoBtn');
|
|
324
|
+
let encryptionTooltip = null;
|
|
325
|
+
|
|
326
|
+
function generateEncryptionContent(encryptionData) {
|
|
327
|
+
const smime = encryptionData?.smime;
|
|
328
|
+
const pgp = encryptionData?.pgp;
|
|
329
|
+
|
|
330
|
+
// Check if any encryption data exists
|
|
331
|
+
const hasEncryptionData = smime || pgp;
|
|
332
|
+
|
|
333
|
+
if (!hasEncryptionData) {
|
|
334
|
+
return `
|
|
335
|
+
<div class="encryption-tooltip-content">
|
|
336
|
+
<p class="encryption-no-data">No encryption or signature information found for this email.</p>
|
|
337
|
+
</div>
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let content = '<div class="encryption-tooltip-content">';
|
|
342
|
+
|
|
343
|
+
if (smime) {
|
|
344
|
+
content += `
|
|
345
|
+
<h3>S/MIME</h3>
|
|
346
|
+
${smime.certificate ? `
|
|
347
|
+
<div class="encryption-info-item">
|
|
348
|
+
<span class="encryption-info-label">Certificate:</span>
|
|
349
|
+
<span class="encryption-info-value">${window.MailCatcherUI.escapeHtml(smime.certificate.substring(0, 40))}...</span>
|
|
350
|
+
</div>
|
|
351
|
+
<button class="encryption-copy-button" data-copy-type="smime-cert" data-value="${window.MailCatcherUI.escapeHtml(smime.certificate)}">
|
|
352
|
+
<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
353
|
+
<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>
|
|
354
|
+
</svg>
|
|
355
|
+
Copy Certificate
|
|
356
|
+
</button>
|
|
357
|
+
` : ''}
|
|
358
|
+
${smime.signature ? `
|
|
359
|
+
<div class="encryption-info-item">
|
|
360
|
+
<span class="encryption-info-label">Signature:</span>
|
|
361
|
+
<span class="encryption-info-value">${window.MailCatcherUI.escapeHtml(smime.signature.substring(0, 40))}...</span>
|
|
362
|
+
</div>
|
|
363
|
+
<button class="encryption-copy-button" data-copy-type="smime-sig" data-value="${window.MailCatcherUI.escapeHtml(smime.signature)}">
|
|
364
|
+
<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
365
|
+
<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>
|
|
366
|
+
</svg>
|
|
367
|
+
Copy Signature
|
|
368
|
+
</button>
|
|
369
|
+
` : ''}
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (pgp) {
|
|
374
|
+
content += `
|
|
375
|
+
<h3>OpenPGP</h3>
|
|
376
|
+
${pgp.key ? `
|
|
377
|
+
<div class="encryption-info-item">
|
|
378
|
+
<span class="encryption-info-label">Key:</span>
|
|
379
|
+
<span class="encryption-info-value">${window.MailCatcherUI.escapeHtml(pgp.key.substring(0, 40))}...</span>
|
|
380
|
+
</div>
|
|
381
|
+
<button class="encryption-copy-button" data-copy-type="pgp-key" data-value="${window.MailCatcherUI.escapeHtml(pgp.key)}">
|
|
382
|
+
<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
383
|
+
<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>
|
|
384
|
+
</svg>
|
|
385
|
+
Copy Key
|
|
386
|
+
</button>
|
|
387
|
+
` : ''}
|
|
388
|
+
${pgp.signature ? `
|
|
389
|
+
<div class="encryption-info-item">
|
|
390
|
+
<span class="encryption-info-label">Signature:</span>
|
|
391
|
+
<span class="encryption-info-value">${window.MailCatcherUI.escapeHtml(pgp.signature.substring(0, 40))}...</span>
|
|
392
|
+
</div>
|
|
393
|
+
<button class="encryption-copy-button" data-copy-type="pgp-sig" data-value="${window.MailCatcherUI.escapeHtml(pgp.signature)}">
|
|
394
|
+
<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
395
|
+
<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>
|
|
396
|
+
</svg>
|
|
397
|
+
Copy Signature
|
|
398
|
+
</button>
|
|
399
|
+
` : ''}
|
|
400
|
+
`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
content += '</div>';
|
|
404
|
+
return content;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (encryptionInfoBtn) {
|
|
408
|
+
encryptionInfoBtn.addEventListener('click', function(e) {
|
|
409
|
+
e.preventDefault();
|
|
410
|
+
if (window.MailCatcher) {
|
|
411
|
+
const messageId = window.MailCatcher.selectedMessage();
|
|
412
|
+
if (!messageId) return;
|
|
413
|
+
|
|
414
|
+
// Destroy existing tooltip if any
|
|
415
|
+
if (encryptionTooltip) {
|
|
416
|
+
encryptionTooltip.destroy();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Fetch message data
|
|
420
|
+
fetch(new URL(`messages/${messageId}.json`, document.baseURI).toString())
|
|
421
|
+
.then(response => response.json())
|
|
422
|
+
.then(data => {
|
|
423
|
+
const encryptionData = data.encryption_data || {};
|
|
424
|
+
const content = generateEncryptionContent(encryptionData);
|
|
425
|
+
|
|
426
|
+
// Create tooltip with content
|
|
427
|
+
encryptionTooltip = tippy(encryptionInfoBtn, {
|
|
428
|
+
content: content,
|
|
429
|
+
allowHTML: true,
|
|
430
|
+
theme: 'light',
|
|
431
|
+
placement: 'bottom-start',
|
|
432
|
+
interactive: true,
|
|
433
|
+
duration: [200, 150],
|
|
434
|
+
arrow: true,
|
|
435
|
+
trigger: 'manual',
|
|
436
|
+
maxWidth: 400,
|
|
437
|
+
onClickOutside: (instance) => {
|
|
438
|
+
instance.hide();
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Show tooltip
|
|
443
|
+
encryptionTooltip.show();
|
|
444
|
+
|
|
445
|
+
// Setup copy button handlers
|
|
446
|
+
const copyButtons = document.querySelectorAll('.encryption-copy-button');
|
|
447
|
+
copyButtons.forEach(btn => {
|
|
448
|
+
btn.addEventListener('click', function(e) {
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
e.stopPropagation();
|
|
451
|
+
|
|
452
|
+
const value = this.getAttribute('data-value');
|
|
453
|
+
const copyType = this.getAttribute('data-copy-type');
|
|
454
|
+
|
|
455
|
+
// Copy to clipboard
|
|
456
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
457
|
+
// Show success state
|
|
458
|
+
const originalHTML = this.innerHTML;
|
|
459
|
+
const originalClass = this.className;
|
|
460
|
+
|
|
461
|
+
this.classList.add('copied');
|
|
462
|
+
this.innerHTML = `
|
|
463
|
+
<svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
464
|
+
<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>
|
|
465
|
+
</svg>
|
|
466
|
+
Copied!
|
|
467
|
+
`;
|
|
468
|
+
|
|
469
|
+
// Reset after 3 seconds
|
|
470
|
+
setTimeout(() => {
|
|
471
|
+
this.className = originalClass;
|
|
472
|
+
this.innerHTML = originalHTML;
|
|
473
|
+
}, 3000);
|
|
474
|
+
}).catch(err => {
|
|
475
|
+
console.error('Failed to copy to clipboard:', err);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
})
|
|
480
|
+
.catch(error => {
|
|
481
|
+
console.error('Error fetching encryption data:', error);
|
|
482
|
+
const errorContent = `
|
|
483
|
+
<div class="encryption-tooltip-content">
|
|
484
|
+
<p class="encryption-no-data">Error loading encryption data.</p>
|
|
485
|
+
</div>
|
|
486
|
+
`;
|
|
487
|
+
|
|
488
|
+
encryptionTooltip = tippy(encryptionInfoBtn, {
|
|
489
|
+
content: errorContent,
|
|
490
|
+
allowHTML: true,
|
|
491
|
+
theme: 'light',
|
|
492
|
+
placement: 'bottom-start',
|
|
493
|
+
interactive: true,
|
|
494
|
+
duration: [200, 150],
|
|
495
|
+
arrow: true,
|
|
496
|
+
trigger: 'manual',
|
|
497
|
+
onClickOutside: (instance) => {
|
|
498
|
+
instance.hide();
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
encryptionTooltip.show();
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Close tooltip when clicking elsewhere
|
|
508
|
+
document.addEventListener('click', function(e) {
|
|
509
|
+
// Check if this click is on the button
|
|
510
|
+
if (encryptionInfoBtn && encryptionInfoBtn.contains(e.target)) return;
|
|
511
|
+
|
|
512
|
+
// Check if this click is on a tippy-box
|
|
513
|
+
if (e.target.closest('.tippy-box')) return;
|
|
514
|
+
|
|
515
|
+
// Click is outside everything, hide tooltip
|
|
516
|
+
if (encryptionTooltip) {
|
|
517
|
+
encryptionTooltip.hide();
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
// Initialize all UI components on page load
|
|
528
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
529
|
+
// Initialize UI components in order
|
|
530
|
+
window.MailCatcherUI.initializeResizer();
|
|
531
|
+
window.MailCatcherUI.initializeUIHandlers();
|
|
532
|
+
window.MailCatcherUI.initializeSignatureTooltip();
|
|
533
|
+
window.MailCatcherUI.initializeEncryptionTooltip();
|
|
534
|
+
|
|
535
|
+
console.log('[MailCatcher] UI components initialized');
|
|
536
|
+
});
|