mailcatcher-ng 1.2.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/mail.rb +83 -0
- data/lib/mail_catcher/smtp.rb +220 -10
- data/lib/mail_catcher/version.rb +1 -1
- data/lib/mail_catcher/web/application.rb +23 -2
- 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 +5 -1622
- data/views/transcript.erb +240 -0
- metadata +4 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<style>
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 20px 28px;
|
|
9
|
+
font-family: 'Monaco', 'Courier New', 'Consolas', monospace;
|
|
10
|
+
font-size: 12px;
|
|
11
|
+
line-height: 1.6;
|
|
12
|
+
background: #ffffff;
|
|
13
|
+
}
|
|
14
|
+
.transcript-header {
|
|
15
|
+
background: #f9f9f9;
|
|
16
|
+
border: 1px solid #e8eaed;
|
|
17
|
+
border-radius: 8px;
|
|
18
|
+
padding: 16px 20px;
|
|
19
|
+
margin-bottom: 20px;
|
|
20
|
+
}
|
|
21
|
+
.transcript-header h3 {
|
|
22
|
+
font-size: 12px;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
text-transform: uppercase;
|
|
25
|
+
letter-spacing: 0.5px;
|
|
26
|
+
color: #5f5f5f;
|
|
27
|
+
margin: 0 0 12px 0;
|
|
28
|
+
}
|
|
29
|
+
.transcript-info-grid {
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
32
|
+
gap: 12px;
|
|
33
|
+
}
|
|
34
|
+
.transcript-info-item {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
gap: 4px;
|
|
38
|
+
}
|
|
39
|
+
.transcript-info-label {
|
|
40
|
+
font-size: 10px;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
color: #999;
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 0.5px;
|
|
45
|
+
}
|
|
46
|
+
.transcript-info-value {
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
color: #1a1a1a;
|
|
49
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
50
|
+
word-break: break-all;
|
|
51
|
+
}
|
|
52
|
+
.transcript-search-box {
|
|
53
|
+
margin-bottom: 16px;
|
|
54
|
+
position: relative;
|
|
55
|
+
}
|
|
56
|
+
.transcript-search-box input {
|
|
57
|
+
width: 100%;
|
|
58
|
+
padding: 10px 36px 10px 12px;
|
|
59
|
+
border: 1px solid #e0e0e0;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
font-size: 13px;
|
|
62
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto';
|
|
63
|
+
background: #ffffff;
|
|
64
|
+
}
|
|
65
|
+
.transcript-search-box input:focus {
|
|
66
|
+
outline: none;
|
|
67
|
+
border-color: #2196F3;
|
|
68
|
+
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
|
|
69
|
+
}
|
|
70
|
+
.transcript-log {
|
|
71
|
+
background: #f9f9f9;
|
|
72
|
+
border: 1px solid #e8eaed;
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
padding: 16px;
|
|
75
|
+
max-height: 600px;
|
|
76
|
+
overflow-y: auto;
|
|
77
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
78
|
+
}
|
|
79
|
+
.transcript-entry {
|
|
80
|
+
display: flex;
|
|
81
|
+
gap: 12px;
|
|
82
|
+
margin: 6px 0;
|
|
83
|
+
padding: 4px 0;
|
|
84
|
+
border-bottom: 1px solid #f0f0f0;
|
|
85
|
+
}
|
|
86
|
+
.transcript-entry:last-child {
|
|
87
|
+
border-bottom: none;
|
|
88
|
+
}
|
|
89
|
+
.transcript-entry.hidden {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
.transcript-time {
|
|
93
|
+
color: #999;
|
|
94
|
+
min-width: 100px;
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
font-size: 11px;
|
|
97
|
+
}
|
|
98
|
+
.transcript-type {
|
|
99
|
+
min-width: 80px;
|
|
100
|
+
flex-shrink: 0;
|
|
101
|
+
font-size: 11px;
|
|
102
|
+
font-weight: 600;
|
|
103
|
+
text-transform: uppercase;
|
|
104
|
+
}
|
|
105
|
+
.transcript-type.connection { color: #9c27b0; }
|
|
106
|
+
.transcript-type.command { color: #2196F3; }
|
|
107
|
+
.transcript-type.response { color: #34a853; }
|
|
108
|
+
.transcript-type.tls { color: #ff9800; }
|
|
109
|
+
.transcript-type.data { color: #607d8b; }
|
|
110
|
+
.transcript-type.error { color: #f44336; }
|
|
111
|
+
.transcript-direction {
|
|
112
|
+
min-width: 60px;
|
|
113
|
+
flex-shrink: 0;
|
|
114
|
+
font-size: 11px;
|
|
115
|
+
color: #666;
|
|
116
|
+
}
|
|
117
|
+
.transcript-direction.client::before {
|
|
118
|
+
content: '→ ';
|
|
119
|
+
color: #2196F3;
|
|
120
|
+
}
|
|
121
|
+
.transcript-direction.server::before {
|
|
122
|
+
content: '← ';
|
|
123
|
+
color: #34a853;
|
|
124
|
+
}
|
|
125
|
+
.transcript-message {
|
|
126
|
+
flex: 1;
|
|
127
|
+
color: #1a1a1a;
|
|
128
|
+
font-size: 12px;
|
|
129
|
+
word-break: break-word;
|
|
130
|
+
white-space: pre-wrap;
|
|
131
|
+
}
|
|
132
|
+
.transcript-message.error {
|
|
133
|
+
color: #f44336;
|
|
134
|
+
font-weight: 500;
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
137
|
+
</head>
|
|
138
|
+
<body>
|
|
139
|
+
<div class="transcript-header">
|
|
140
|
+
<h3>SMTP Session Information</h3>
|
|
141
|
+
<div class="transcript-info-grid">
|
|
142
|
+
<div class="transcript-info-item">
|
|
143
|
+
<div class="transcript-info-label">Client</div>
|
|
144
|
+
<div class="transcript-info-value"><%= transcript['client_ip'] %>:<%= transcript['client_port'] %></div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="transcript-info-item">
|
|
147
|
+
<div class="transcript-info-label">Server</div>
|
|
148
|
+
<div class="transcript-info-value"><%= transcript['server_ip'] %>:<%= transcript['server_port'] %></div>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="transcript-info-item">
|
|
151
|
+
<div class="transcript-info-label">Session ID</div>
|
|
152
|
+
<div class="transcript-info-value"><%= transcript['session_id'] %></div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="transcript-info-item">
|
|
155
|
+
<div class="transcript-info-label">TLS</div>
|
|
156
|
+
<div class="transcript-info-value">
|
|
157
|
+
<% if transcript['tls_enabled'] %>
|
|
158
|
+
✓ <%= transcript['tls_protocol'] || 'Enabled' %>
|
|
159
|
+
<% else %>
|
|
160
|
+
✗ Not used
|
|
161
|
+
<% end %>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<% if transcript['tls_enabled'] && transcript['tls_cipher'] %>
|
|
165
|
+
<div class="transcript-info-item">
|
|
166
|
+
<div class="transcript-info-label">Cipher</div>
|
|
167
|
+
<div class="transcript-info-value"><%= transcript['tls_cipher'] %></div>
|
|
168
|
+
</div>
|
|
169
|
+
<% end %>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="transcript-search-box" style="width: 50%;">
|
|
174
|
+
<input type="text" id="transcriptSearch" placeholder="Filter transcript entries..." style="width: 100%;" />
|
|
175
|
+
<button class="transcript-search-clear" id="transcriptSearchClear" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #999; cursor: pointer; display: none; font-size: 18px; padding: 0; width: 20px; height: 20px;">×</button>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="transcript-log">
|
|
179
|
+
<% if transcript['entries'] && transcript['entries'].length > 0 %>
|
|
180
|
+
<% transcript['entries'].each do |entry| %>
|
|
181
|
+
<div class="transcript-entry" data-searchable="<%= CGI.escapeHTML(entry.to_json.downcase) %>">
|
|
182
|
+
<div class="transcript-time">
|
|
183
|
+
<%
|
|
184
|
+
time = Time.parse(entry['timestamp'])
|
|
185
|
+
hours = time.strftime('%H')
|
|
186
|
+
minutes = time.strftime('%M')
|
|
187
|
+
seconds = time.strftime('%S')
|
|
188
|
+
ms = (time.usec / 1000).to_s.rjust(3, '0')
|
|
189
|
+
%>
|
|
190
|
+
<%= "#{hours}:#{minutes}:#{seconds}.#{ms}" %>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="transcript-type <%= entry['type'] %>"><%= entry['type'] %></div>
|
|
193
|
+
<div class="transcript-direction <%= entry['direction'] %>"><%= entry['direction'] %></div>
|
|
194
|
+
<div class="transcript-message <%= 'error' if entry['type'] == 'error' %>"><%= CGI.escapeHTML(entry['message']) %></div>
|
|
195
|
+
</div>
|
|
196
|
+
<% end %>
|
|
197
|
+
<% else %>
|
|
198
|
+
<div style="text-align: center; padding: 40px 20px; color: #999; font-size: 14px;">
|
|
199
|
+
No transcript entries found.
|
|
200
|
+
</div>
|
|
201
|
+
<% end %>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<script>
|
|
205
|
+
var searchInput = document.getElementById('transcriptSearch');
|
|
206
|
+
var searchClear = document.getElementById('transcriptSearchClear');
|
|
207
|
+
var entries = document.querySelectorAll('.transcript-entry');
|
|
208
|
+
|
|
209
|
+
function filterEntries() {
|
|
210
|
+
var query = searchInput.value.toLowerCase().trim();
|
|
211
|
+
entries.forEach(function(entry) {
|
|
212
|
+
if (!query) {
|
|
213
|
+
entry.classList.remove('hidden');
|
|
214
|
+
} else {
|
|
215
|
+
var searchable = entry.getAttribute('data-searchable');
|
|
216
|
+
if (searchable.indexOf(query) >= 0) {
|
|
217
|
+
entry.classList.remove('hidden');
|
|
218
|
+
} else {
|
|
219
|
+
entry.classList.add('hidden');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
searchClear.style.display = query ? 'block' : 'none';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
searchInput.addEventListener('keyup', filterEntries);
|
|
227
|
+
searchClear.addEventListener('click', function() {
|
|
228
|
+
searchInput.value = '';
|
|
229
|
+
filterEntries();
|
|
230
|
+
searchInput.focus();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Auto-scroll to bottom
|
|
234
|
+
var log = document.querySelector('.transcript-log');
|
|
235
|
+
if (log) {
|
|
236
|
+
log.scrollTop = log.scrollHeight;
|
|
237
|
+
}
|
|
238
|
+
</script>
|
|
239
|
+
</body>
|
|
240
|
+
</html>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mailcatcher-ng
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephane Paquet
|
|
@@ -353,11 +353,14 @@ files:
|
|
|
353
353
|
- lib/mailcatcher.rb
|
|
354
354
|
- public/assets/atom-one-light.min.css
|
|
355
355
|
- public/assets/highlight.min.js
|
|
356
|
+
- public/assets/mailcatcher-ui.js
|
|
357
|
+
- public/assets/mailcatcher.css
|
|
356
358
|
- public/assets/mailcatcher.js
|
|
357
359
|
- public/favicon.ico
|
|
358
360
|
- views/404.erb
|
|
359
361
|
- views/index.erb
|
|
360
362
|
- views/server_info.erb
|
|
363
|
+
- views/transcript.erb
|
|
361
364
|
- views/websocket_test.erb
|
|
362
365
|
homepage: https://spaquet.github.io/mailcatcher/
|
|
363
366
|
licenses:
|