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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6539793c4e31686b97ba0502cc4c48c47c49ace16230fea4cf2f2cb8d06704c7
4
- data.tar.gz: d74ed846c8205f2b37b9b433b618424929d12515a928d23b4a481fe03a5993d1
3
+ metadata.gz: 2238cc35e17eb1bca222b4adf177bd093f9eacf291f5b179c3c1db7cc6ccfc53
4
+ data.tar.gz: 6d7487a8ef8f6488afa6f83e850c2c3f928424cd870ed313c15849c849873672
5
5
  SHA512:
6
- metadata.gz: fd19f4d57d9710adc6c60a21aeeefbfb2623e57d6725463f5bd01b3ce5e5fcb93360d1543301bc9655b64fc814994d036e3c8cf8de3940643f69ccf1f4063297
7
- data.tar.gz: 7aed22f4e9a54ca7e239f7401d95931cfce90f309d2beae5d28049aa939b4903bb2d256aac8ff694c3740bf62835d6bf2c5f90d8c9b53fd3f855a2b282e6860e
6
+ metadata.gz: f784c55cf0eb84d166a48ba99e36e60b3fd4028facf5e2b30fa92c3c6f17814f71434fb6e5be2f68a82c4c3f3b93c7cc2c617c5100a8515ba0e9d2a3331e3272
7
+ data.tar.gz: 776268ce8b6c14dcb67c5387e03611015c1dab6afc8527ef6a1bd67d42bd510d634eec220df5363625de4536f576e26fd53605140a00a2aaddac2e4190c1fe55
@@ -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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MailCatcher
4
- VERSION = '1.4.0'
4
+ VERSION = '1.4.6'
5
5
  end
@@ -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
- starttls: :required,
351
- starttls_options: ssl_options
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
- starttls: :required,
357
- starttls_options: ssl_options
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);