mailcatcher-ng 1.4.0 → 1.4.6
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 +52 -0
- data/lib/mail_catcher/version.rb +1 -1
- data/lib/mail_catcher/web/application.rb +41 -3
- data/lib/mail_catcher.rb +7 -7
- data/public/assets/atom-one-light.min.css +1 -1
- data/public/assets/favcount.js +102 -0
- data/public/assets/highlight.min.js +370 -401
- data/public/assets/jquery.min.js +2 -0
- data/public/assets/jquery.min.map +1 -0
- data/public/assets/mailcatcher-ui.js +3 -0
- data/public/assets/mailcatcher.css +43 -0
- data/public/assets/mailcatcher.js +1185 -22
- data/public/assets/popper.min.js +6 -0
- data/public/assets/popper.min.js.map +1 -0
- data/public/assets/tippy-light.min.css +1 -0
- data/public/assets/tippy.min.js +2 -0
- data/views/index.erb +20 -7
- data/views/server_info.erb +20 -8
- metadata +8 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2238cc35e17eb1bca222b4adf177bd093f9eacf291f5b179c3c1db7cc6ccfc53
|
|
4
|
+
data.tar.gz: 6d7487a8ef8f6488afa6f83e850c2c3f928424cd870ed313c15849c849873672
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f784c55cf0eb84d166a48ba99e36e60b3fd4028facf5e2b30fa92c3c6f17814f71434fb6e5be2f68a82c4c3f3b93c7cc2c617c5100a8515ba0e9d2a3331e3272
|
|
7
|
+
data.tar.gz: 776268ce8b6c14dcb67c5387e03611015c1dab6afc8527ef6a1bd67d42bd510d634eec220df5363625de4536f576e26fd53605140a00a2aaddac2e4190c1fe55
|
data/lib/mail_catcher/mail.rb
CHANGED
|
@@ -57,8 +57,24 @@ module MailCatcher::Mail extend self
|
|
|
57
57
|
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
|
|
58
58
|
)
|
|
59
59
|
SQL
|
|
60
|
+
db.execute(<<-SQL)
|
|
61
|
+
CREATE TABLE IF NOT EXISTS websocket_connection (
|
|
62
|
+
id INTEGER PRIMARY KEY ASC,
|
|
63
|
+
session_id TEXT NOT NULL,
|
|
64
|
+
client_ip TEXT,
|
|
65
|
+
opened_at DATETIME,
|
|
66
|
+
closed_at DATETIME,
|
|
67
|
+
last_ping_at DATETIME,
|
|
68
|
+
last_pong_at DATETIME,
|
|
69
|
+
ping_count INTEGER DEFAULT 0,
|
|
70
|
+
pong_count INTEGER DEFAULT 0,
|
|
71
|
+
created_at DATETIME DEFAULT CURRENT_DATETIME,
|
|
72
|
+
updated_at DATETIME DEFAULT CURRENT_DATETIME
|
|
73
|
+
)
|
|
74
|
+
SQL
|
|
60
75
|
db.execute("CREATE INDEX IF NOT EXISTS idx_smtp_transcript_message_id ON smtp_transcript(message_id)")
|
|
61
76
|
db.execute("CREATE INDEX IF NOT EXISTS idx_smtp_transcript_session_id ON smtp_transcript(session_id)")
|
|
77
|
+
db.execute("CREATE INDEX IF NOT EXISTS idx_websocket_connection_session_id ON websocket_connection(session_id)")
|
|
62
78
|
db.execute("PRAGMA foreign_keys = ON")
|
|
63
79
|
end
|
|
64
80
|
end
|
|
@@ -505,6 +521,42 @@ module MailCatcher::Mail extend self
|
|
|
505
521
|
end
|
|
506
522
|
end
|
|
507
523
|
|
|
524
|
+
def create_websocket_connection(session_id, client_ip)
|
|
525
|
+
@create_ws_connection_query ||= db.prepare(<<-SQL)
|
|
526
|
+
INSERT INTO websocket_connection (session_id, client_ip, opened_at, created_at, updated_at)
|
|
527
|
+
VALUES (?, ?, datetime('now'), datetime('now'), datetime('now'))
|
|
528
|
+
SQL
|
|
529
|
+
@create_ws_connection_query.execute(session_id, client_ip)
|
|
530
|
+
db.last_insert_row_id
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def close_websocket_connection(session_id)
|
|
534
|
+
@close_ws_connection_query ||= db.prepare(<<-SQL)
|
|
535
|
+
UPDATE websocket_connection
|
|
536
|
+
SET closed_at = datetime('now'), updated_at = datetime('now')
|
|
537
|
+
WHERE session_id = ? AND closed_at IS NULL
|
|
538
|
+
SQL
|
|
539
|
+
@close_ws_connection_query.execute(session_id)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def record_websocket_ping(session_id)
|
|
543
|
+
@record_ping_query ||= db.prepare(<<-SQL)
|
|
544
|
+
UPDATE websocket_connection
|
|
545
|
+
SET last_ping_at = datetime('now'), ping_count = ping_count + 1, updated_at = datetime('now')
|
|
546
|
+
WHERE session_id = ? AND closed_at IS NULL
|
|
547
|
+
SQL
|
|
548
|
+
@record_ping_query.execute(session_id)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def record_websocket_pong(session_id)
|
|
552
|
+
@record_pong_query ||= db.prepare(<<-SQL)
|
|
553
|
+
UPDATE websocket_connection
|
|
554
|
+
SET last_pong_at = datetime('now'), pong_count = pong_count + 1, updated_at = datetime('now')
|
|
555
|
+
WHERE session_id = ? AND closed_at IS NULL
|
|
556
|
+
SQL
|
|
557
|
+
@record_pong_query.execute(session_id)
|
|
558
|
+
end
|
|
559
|
+
|
|
508
560
|
private
|
|
509
561
|
|
|
510
562
|
def parse_authentication_results(auth_header)
|
data/lib/mail_catcher/version.rb
CHANGED
|
@@ -73,6 +73,13 @@ module MailCatcher
|
|
|
73
73
|
erb :websocket_test
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
get "/version.json" do
|
|
77
|
+
content_type :json
|
|
78
|
+
JSON.generate({
|
|
79
|
+
version: MailCatcher::VERSION
|
|
80
|
+
})
|
|
81
|
+
end
|
|
82
|
+
|
|
76
83
|
get "/server-info" do
|
|
77
84
|
@version = MailCatcher::VERSION
|
|
78
85
|
@smtp_ip = MailCatcher.options[:smtp_ip]
|
|
@@ -146,11 +153,16 @@ module MailCatcher
|
|
|
146
153
|
get "/messages" do
|
|
147
154
|
if request.websocket?
|
|
148
155
|
bus_subscription = nil
|
|
156
|
+
ping_timer = nil
|
|
157
|
+
session_id = SecureRandom.uuid
|
|
158
|
+
client_ip = request.ip
|
|
149
159
|
|
|
150
160
|
ws = Faye::WebSocket.new(request.env)
|
|
151
161
|
|
|
152
162
|
ws.on(:open) do |_|
|
|
153
|
-
$stderr.puts "[WebSocket] Connection opened"
|
|
163
|
+
$stderr.puts "[WebSocket] Connection opened (session: #{session_id}, ip: #{client_ip})"
|
|
164
|
+
MailCatcher::Mail.create_websocket_connection(session_id, client_ip)
|
|
165
|
+
|
|
154
166
|
bus_subscription = MailCatcher::Bus.subscribe do |message|
|
|
155
167
|
begin
|
|
156
168
|
$stderr.puts "[WebSocket] Sending message: #{message.inspect}"
|
|
@@ -160,15 +172,41 @@ module MailCatcher
|
|
|
160
172
|
MailCatcher.log_exception("Error sending message through websocket", message, exception)
|
|
161
173
|
end
|
|
162
174
|
end
|
|
175
|
+
|
|
176
|
+
# Send initial ping and set up periodic ping timer (every 30 seconds)
|
|
177
|
+
ping_interval = 30
|
|
178
|
+
ping_timer = EventMachine.add_periodic_timer(ping_interval) do
|
|
179
|
+
begin
|
|
180
|
+
$stderr.puts "[WebSocket] Sending ping (session: #{session_id})"
|
|
181
|
+
MailCatcher::Mail.record_websocket_ping(session_id)
|
|
182
|
+
ws.send(JSON.generate({ type: "ping" }))
|
|
183
|
+
rescue => exception
|
|
184
|
+
$stderr.puts "[WebSocket] Error sending ping: #{exception.message}"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
ws.on(:message) do |event|
|
|
190
|
+
begin
|
|
191
|
+
data = JSON.parse(event.data)
|
|
192
|
+
if data["type"] == "pong"
|
|
193
|
+
$stderr.puts "[WebSocket] Received pong (session: #{session_id})"
|
|
194
|
+
MailCatcher::Mail.record_websocket_pong(session_id)
|
|
195
|
+
end
|
|
196
|
+
rescue => exception
|
|
197
|
+
$stderr.puts "[WebSocket] Error processing message: #{exception.message}"
|
|
198
|
+
end
|
|
163
199
|
end
|
|
164
200
|
|
|
165
201
|
ws.on(:close) do |_|
|
|
166
|
-
$stderr.puts "[WebSocket] Connection closed"
|
|
202
|
+
$stderr.puts "[WebSocket] Connection closed (session: #{session_id})"
|
|
203
|
+
EventMachine.cancel_timer(ping_timer) if ping_timer
|
|
167
204
|
MailCatcher::Bus.unsubscribe(bus_subscription) if bus_subscription
|
|
205
|
+
MailCatcher::Mail.close_websocket_connection(session_id)
|
|
168
206
|
end
|
|
169
207
|
|
|
170
208
|
ws.on(:error) do |event|
|
|
171
|
-
$stderr.puts "[WebSocket] WebSocket error: #{event}"
|
|
209
|
+
$stderr.puts "[WebSocket] WebSocket error: #{event} (session: #{session_id})"
|
|
172
210
|
end
|
|
173
211
|
|
|
174
212
|
ws.rack_response
|
data/lib/mail_catcher.rb
CHANGED
|
@@ -110,7 +110,7 @@ module MailCatcher
|
|
|
110
110
|
parser.banner = 'Usage: mailcatcher [options]'
|
|
111
111
|
parser.version = VERSION
|
|
112
112
|
parser.separator ''
|
|
113
|
-
parser.separator "MailCatcher v#{VERSION}"
|
|
113
|
+
parser.separator "MailCatcher NG v#{VERSION}"
|
|
114
114
|
parser.separator ''
|
|
115
115
|
|
|
116
116
|
parser.on('--ip IP', 'Set the ip address of both servers') do |ip|
|
|
@@ -347,15 +347,15 @@ module MailCatcher
|
|
|
347
347
|
|
|
348
348
|
# Configure the STARTTLS Smtp class
|
|
349
349
|
Smtp.class_variable_set(:@@parms, {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
350
|
+
starttls: :required,
|
|
351
|
+
starttls_options: ssl_options
|
|
352
|
+
})
|
|
353
353
|
|
|
354
354
|
# Configure the Direct TLS SmtpTls class
|
|
355
355
|
SmtpTls.class_variable_set(:@@parms, {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
356
|
+
starttls: :required,
|
|
357
|
+
starttls_options: ssl_options
|
|
358
|
+
})
|
|
359
359
|
|
|
360
360
|
@ssl_options = ssl_options
|
|
361
361
|
end
|
|
@@ -1 +1 @@
|
|
|
1
|
-
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
|
|
1
|
+
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* favcount.js v1.5.0
|
|
3
|
+
* http://chrishunt.co/favcount
|
|
4
|
+
* Dynamically updates the favicon with a number.
|
|
5
|
+
*
|
|
6
|
+
* Copyright 2013, Chris Hunt
|
|
7
|
+
* Released under the MIT license
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
(function(){
|
|
11
|
+
function Favcount(icon) {
|
|
12
|
+
this.icon = icon;
|
|
13
|
+
this.opacity = 0.4;
|
|
14
|
+
this.canvas = document.createElement('canvas');
|
|
15
|
+
this.font = "Helvetica, Arial, sans-serif";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Favcount.prototype.set = function(count) {
|
|
19
|
+
var self = this,
|
|
20
|
+
img = document.createElement('img');
|
|
21
|
+
|
|
22
|
+
if (self.canvas.getContext) {
|
|
23
|
+
img.crossOrigin = "anonymous";
|
|
24
|
+
|
|
25
|
+
img.onload = function() {
|
|
26
|
+
drawCanvas(self.canvas, self.opacity, self.font, img, normalize(count));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
img.src = this.icon;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function normalize(count) {
|
|
34
|
+
count = Math.round(count);
|
|
35
|
+
|
|
36
|
+
if (isNaN(count) || count < 1) {
|
|
37
|
+
return '';
|
|
38
|
+
} else if (count < 10) {
|
|
39
|
+
return ' ' + count;
|
|
40
|
+
} else if (count > 99) {
|
|
41
|
+
return '99';
|
|
42
|
+
} else {
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function drawCanvas(canvas, opacity, font, img, count) {
|
|
48
|
+
var head = document.getElementsByTagName('head')[0],
|
|
49
|
+
favicon = document.createElement('link'),
|
|
50
|
+
multiplier, fontSize, context, xOffset, yOffset, border, shadow;
|
|
51
|
+
|
|
52
|
+
favicon.rel = 'icon';
|
|
53
|
+
|
|
54
|
+
// Scale canvas elements based on favicon size
|
|
55
|
+
multiplier = img.width / 16;
|
|
56
|
+
fontSize = multiplier * 11;
|
|
57
|
+
xOffset = multiplier;
|
|
58
|
+
yOffset = multiplier * 11;
|
|
59
|
+
border = multiplier;
|
|
60
|
+
shadow = multiplier * 2;
|
|
61
|
+
|
|
62
|
+
canvas.height = canvas.width = img.width;
|
|
63
|
+
context = canvas.getContext('2d');
|
|
64
|
+
context.font = 'bold ' + fontSize + 'px ' + font;
|
|
65
|
+
|
|
66
|
+
// Draw faded favicon background
|
|
67
|
+
if (count) { context.globalAlpha = opacity; }
|
|
68
|
+
context.drawImage(img, 0, 0);
|
|
69
|
+
context.globalAlpha = 1.0;
|
|
70
|
+
|
|
71
|
+
// Draw white drop shadow
|
|
72
|
+
context.shadowColor = '#FFF';
|
|
73
|
+
context.shadowBlur = shadow;
|
|
74
|
+
context.shadowOffsetX = 0;
|
|
75
|
+
context.shadowOffsetY = 0;
|
|
76
|
+
|
|
77
|
+
// Draw white border
|
|
78
|
+
context.fillStyle = '#FFF';
|
|
79
|
+
context.fillText(count, xOffset, yOffset);
|
|
80
|
+
context.fillText(count, xOffset + border, yOffset);
|
|
81
|
+
context.fillText(count, xOffset, yOffset + border);
|
|
82
|
+
context.fillText(count, xOffset + border, yOffset + border);
|
|
83
|
+
|
|
84
|
+
// Draw black count
|
|
85
|
+
context.fillStyle = '#000';
|
|
86
|
+
context.fillText(count,
|
|
87
|
+
xOffset + (border / 2.0),
|
|
88
|
+
yOffset + (border / 2.0)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Replace favicon with new favicon
|
|
92
|
+
favicon.href = canvas.toDataURL('image/png');
|
|
93
|
+
head.removeChild(document.querySelector('link[rel=icon]'));
|
|
94
|
+
head.appendChild(favicon);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.Favcount = Favcount;
|
|
98
|
+
}).call(this);
|
|
99
|
+
|
|
100
|
+
(function(){
|
|
101
|
+
Favcount.VERSION = '1.5.0';
|
|
102
|
+
}).call(this);
|