mailcatcher-ng 1.1.2 → 1.3.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 +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.js +1 -1
- data/views/index.erb +193 -0
- data/views/transcript.erb +240 -0
- metadata +6 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afadfb9772aa8cd6e53a99e420060b1a3e6c93781b6d81ceaad04ac32bf4ec91
|
|
4
|
+
data.tar.gz: 9eea84760e814326716fb59aac72dc6606b156f3662d24cfe642b7b66b33add0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48d1f1af70237339b6ffa7bbcc2ffcf0fed8b04b04973cec17fdee831d66c9893be184dac7bebe334d273393659e87ab4e44b252b3c2fde27741a6f9c197cb54
|
|
7
|
+
data.tar.gz: 4f28976f41605c8fd3d1f87ba037aa1a75722fdb34d02c7ed42394290be4c2c81c04bd5d201ca38d34eaaf671d1c7c7667a1a51768518636f5ebd1140109b5b6
|
data/lib/mail_catcher/mail.rb
CHANGED
|
@@ -36,6 +36,27 @@ module MailCatcher::Mail extend self
|
|
|
36
36
|
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
|
|
37
37
|
)
|
|
38
38
|
SQL
|
|
39
|
+
db.execute(<<-SQL)
|
|
40
|
+
CREATE TABLE smtp_transcript (
|
|
41
|
+
id INTEGER PRIMARY KEY ASC,
|
|
42
|
+
message_id INTEGER,
|
|
43
|
+
session_id TEXT NOT NULL,
|
|
44
|
+
client_ip TEXT,
|
|
45
|
+
client_port INTEGER,
|
|
46
|
+
server_ip TEXT,
|
|
47
|
+
server_port INTEGER,
|
|
48
|
+
tls_enabled INTEGER DEFAULT 0,
|
|
49
|
+
tls_protocol TEXT,
|
|
50
|
+
tls_cipher TEXT,
|
|
51
|
+
connection_started_at DATETIME,
|
|
52
|
+
connection_ended_at DATETIME,
|
|
53
|
+
entries TEXT NOT NULL,
|
|
54
|
+
created_at DATETIME DEFAULT CURRENT_DATETIME,
|
|
55
|
+
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
|
|
56
|
+
)
|
|
57
|
+
SQL
|
|
58
|
+
db.execute("CREATE INDEX idx_smtp_transcript_message_id ON smtp_transcript(message_id)")
|
|
59
|
+
db.execute("CREATE INDEX idx_smtp_transcript_session_id ON smtp_transcript(session_id)")
|
|
39
60
|
db.execute("PRAGMA foreign_keys = ON")
|
|
40
61
|
end
|
|
41
62
|
end
|
|
@@ -60,6 +81,8 @@ module MailCatcher::Mail extend self
|
|
|
60
81
|
message = MailCatcher::Mail.message message_id
|
|
61
82
|
MailCatcher::Bus.push(type: "add", message: message)
|
|
62
83
|
end
|
|
84
|
+
|
|
85
|
+
message_id
|
|
63
86
|
end
|
|
64
87
|
|
|
65
88
|
def add_message_part(*args)
|
|
@@ -387,6 +410,66 @@ module MailCatcher::Mail extend self
|
|
|
387
410
|
encryption_data
|
|
388
411
|
end
|
|
389
412
|
|
|
413
|
+
def add_smtp_transcript(params)
|
|
414
|
+
@add_smtp_transcript_query ||= db.prepare(<<-SQL)
|
|
415
|
+
INSERT INTO smtp_transcript (
|
|
416
|
+
message_id, session_id, client_ip, client_port,
|
|
417
|
+
server_ip, server_port, tls_enabled, tls_protocol,
|
|
418
|
+
tls_cipher, connection_started_at, connection_ended_at,
|
|
419
|
+
entries, created_at
|
|
420
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
421
|
+
SQL
|
|
422
|
+
|
|
423
|
+
@add_smtp_transcript_query.execute(
|
|
424
|
+
params[:message_id],
|
|
425
|
+
params[:session_id],
|
|
426
|
+
params[:client_ip],
|
|
427
|
+
params[:client_port],
|
|
428
|
+
params[:server_ip],
|
|
429
|
+
params[:server_port],
|
|
430
|
+
params[:tls_enabled],
|
|
431
|
+
params[:tls_protocol],
|
|
432
|
+
params[:tls_cipher],
|
|
433
|
+
params[:connection_started_at]&.utc&.iso8601,
|
|
434
|
+
params[:connection_ended_at]&.utc&.iso8601,
|
|
435
|
+
JSON.generate(params[:entries])
|
|
436
|
+
)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def message_transcript(message_id)
|
|
440
|
+
@message_transcript_query ||= db.prepare(<<-SQL)
|
|
441
|
+
SELECT id, session_id, client_ip, client_port,
|
|
442
|
+
server_ip, server_port, tls_enabled, tls_protocol,
|
|
443
|
+
tls_cipher, connection_started_at, connection_ended_at,
|
|
444
|
+
entries, created_at
|
|
445
|
+
FROM smtp_transcript
|
|
446
|
+
WHERE message_id = ?
|
|
447
|
+
ORDER BY created_at DESC
|
|
448
|
+
LIMIT 1
|
|
449
|
+
SQL
|
|
450
|
+
|
|
451
|
+
row = @message_transcript_query.execute(message_id).next
|
|
452
|
+
return nil unless row
|
|
453
|
+
|
|
454
|
+
result = Hash[@message_transcript_query.columns.zip(row)]
|
|
455
|
+
result['entries'] = JSON.parse(result['entries']) if result['entries']
|
|
456
|
+
result['tls_enabled'] = result['tls_enabled'] == 1
|
|
457
|
+
result
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def all_transcripts
|
|
461
|
+
@all_transcripts_query ||= db.prepare(<<-SQL)
|
|
462
|
+
SELECT id, message_id, session_id, client_ip,
|
|
463
|
+
connection_started_at, tls_enabled
|
|
464
|
+
FROM smtp_transcript
|
|
465
|
+
ORDER BY created_at DESC
|
|
466
|
+
SQL
|
|
467
|
+
|
|
468
|
+
@all_transcripts_query.execute.map do |row|
|
|
469
|
+
Hash[@all_transcripts_query.columns.zip(row)]
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
390
473
|
private
|
|
391
474
|
|
|
392
475
|
def parse_authentication_results(auth_header)
|
data/lib/mail_catcher/smtp.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "eventmachine"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "socket"
|
|
4
6
|
|
|
5
7
|
require "mail_catcher/mail"
|
|
6
8
|
|
|
@@ -11,16 +13,111 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
11
13
|
@@active_connections
|
|
12
14
|
end
|
|
13
15
|
|
|
16
|
+
def initialize(*args)
|
|
17
|
+
@transcript_entries = []
|
|
18
|
+
@session_id = SecureRandom.uuid
|
|
19
|
+
@connection_started_at = Time.now
|
|
20
|
+
@data_started = false
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
14
24
|
def post_init
|
|
15
25
|
@@active_connections += 1
|
|
26
|
+
|
|
27
|
+
# Get connection details
|
|
28
|
+
begin
|
|
29
|
+
peer_sockaddr = get_peername
|
|
30
|
+
if peer_sockaddr
|
|
31
|
+
port, client_ip = Socket.unpack_sockaddr_in(peer_sockaddr)
|
|
32
|
+
@client_ip = client_ip
|
|
33
|
+
@client_port = port
|
|
34
|
+
end
|
|
35
|
+
rescue => e
|
|
36
|
+
$stderr.puts "Error getting peer info: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
local_sockaddr = get_sockname
|
|
41
|
+
if local_sockaddr
|
|
42
|
+
port, server_ip = Socket.unpack_sockaddr_in(local_sockaddr)
|
|
43
|
+
@server_ip = server_ip
|
|
44
|
+
@server_port = port
|
|
45
|
+
end
|
|
46
|
+
rescue => e
|
|
47
|
+
$stderr.puts "Error getting local info: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
log_transcript('connection', 'server', "Connection established from #{@client_ip}:#{@client_port}")
|
|
51
|
+
|
|
16
52
|
super
|
|
17
53
|
end
|
|
18
54
|
|
|
19
55
|
def unbind
|
|
20
56
|
@@active_connections -= 1
|
|
57
|
+
|
|
58
|
+
@connection_ended_at = Time.now
|
|
59
|
+
log_transcript('connection', 'server', "Connection closed")
|
|
60
|
+
|
|
61
|
+
# Save transcript even if no message was completed
|
|
62
|
+
save_transcript(nil) if @transcript_entries.any?
|
|
63
|
+
|
|
21
64
|
super
|
|
22
65
|
end
|
|
23
66
|
|
|
67
|
+
def log_transcript(type, direction, message)
|
|
68
|
+
@transcript_entries << {
|
|
69
|
+
timestamp: Time.now.utc.iso8601(3),
|
|
70
|
+
type: type,
|
|
71
|
+
direction: direction,
|
|
72
|
+
message: message
|
|
73
|
+
}
|
|
74
|
+
rescue => e
|
|
75
|
+
$stderr.puts "Error logging transcript: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def save_transcript(message_id)
|
|
79
|
+
return if @transcript_entries.empty?
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
tls_enabled = @tls_started ? 1 : 0
|
|
83
|
+
tls_protocol = nil
|
|
84
|
+
tls_cipher = nil
|
|
85
|
+
|
|
86
|
+
if @tls_started
|
|
87
|
+
begin
|
|
88
|
+
tls_protocol = get_cipher_protocol
|
|
89
|
+
rescue
|
|
90
|
+
# TLS info not available
|
|
91
|
+
end
|
|
92
|
+
begin
|
|
93
|
+
tls_cipher = get_cipher_name
|
|
94
|
+
rescue
|
|
95
|
+
# TLS info not available
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
MailCatcher::Mail.add_smtp_transcript(
|
|
100
|
+
message_id: message_id,
|
|
101
|
+
session_id: @session_id,
|
|
102
|
+
client_ip: @client_ip,
|
|
103
|
+
client_port: @client_port,
|
|
104
|
+
server_ip: @server_ip,
|
|
105
|
+
server_port: @server_port,
|
|
106
|
+
tls_enabled: tls_enabled,
|
|
107
|
+
tls_protocol: tls_protocol,
|
|
108
|
+
tls_cipher: tls_cipher,
|
|
109
|
+
connection_started_at: @connection_started_at,
|
|
110
|
+
connection_ended_at: @connection_ended_at || Time.now,
|
|
111
|
+
entries: @transcript_entries
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@transcript_entries = []
|
|
115
|
+
rescue => e
|
|
116
|
+
$stderr.puts "Error saving SMTP transcript: #{e.message}"
|
|
117
|
+
$stderr.puts e.backtrace.join("\n")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
24
121
|
# We override EM's mail from processing to allow multiple mail-from commands
|
|
25
122
|
# per [RFC 2821](https://tools.ietf.org/html/rfc2821#section-4.1.1.2)
|
|
26
123
|
def process_mail_from sender
|
|
@@ -38,7 +135,11 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
38
135
|
end
|
|
39
136
|
|
|
40
137
|
def receive_reset
|
|
138
|
+
log_transcript('command', 'client', 'RSET')
|
|
139
|
+
log_transcript('response', 'server', '250 OK')
|
|
140
|
+
|
|
41
141
|
@current_message = nil
|
|
142
|
+
@data_started = false
|
|
42
143
|
|
|
43
144
|
true
|
|
44
145
|
end
|
|
@@ -48,33 +149,46 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
48
149
|
# SIZE: RFC 1870 - Message size extension
|
|
49
150
|
# 8BITMIME: RFC 6152 - 8bit MIME transport
|
|
50
151
|
# SMTPUTF8: RFC 6531 - UTF-8 support in SMTP
|
|
51
|
-
|
|
52
|
-
capabilities << "8BITMIME"
|
|
53
|
-
capabilities << "SMTPUTF8"
|
|
54
|
-
capabilities
|
|
152
|
+
["8BITMIME", "SMTPUTF8"]
|
|
55
153
|
end
|
|
56
154
|
|
|
57
155
|
def receive_sender(sender)
|
|
156
|
+
# Log the full MAIL FROM command with ESMTP parameters
|
|
157
|
+
log_transcript('command', 'client', "MAIL FROM:<#{sender}>")
|
|
158
|
+
|
|
58
159
|
# EventMachine SMTP advertises size extensions [https://tools.ietf.org/html/rfc1870]
|
|
59
160
|
# and other SMTP parameters via the MAIL FROM command
|
|
60
161
|
# Strip potential " SIZE=..." and "BODY=..." suffixes from senders
|
|
61
|
-
|
|
162
|
+
sender_cleaned = sender.gsub(/ (?:SIZE|BODY)=\S+/i, "")
|
|
163
|
+
|
|
164
|
+
log_transcript('response', 'server', '250 OK')
|
|
62
165
|
|
|
63
|
-
current_message[:sender] =
|
|
166
|
+
current_message[:sender] = sender_cleaned
|
|
64
167
|
# Store the original sender line to track if 8BIT was specified
|
|
65
|
-
current_message[:sender_line] =
|
|
168
|
+
current_message[:sender_line] = sender_cleaned
|
|
66
169
|
|
|
67
170
|
true
|
|
68
171
|
end
|
|
69
172
|
|
|
70
173
|
def receive_recipient(recipient)
|
|
174
|
+
log_transcript('command', 'client', "RCPT TO:<#{recipient}>")
|
|
175
|
+
|
|
71
176
|
current_message[:recipients] ||= []
|
|
72
177
|
current_message[:recipients] << recipient
|
|
73
178
|
|
|
179
|
+
log_transcript('response', 'server', '250 OK')
|
|
180
|
+
|
|
74
181
|
true
|
|
75
182
|
end
|
|
76
183
|
|
|
77
184
|
def receive_data_chunk(lines)
|
|
185
|
+
# Log DATA command on first chunk only
|
|
186
|
+
if !@data_started
|
|
187
|
+
@data_started = true
|
|
188
|
+
log_transcript('command', 'client', 'DATA')
|
|
189
|
+
log_transcript('response', 'server', '354 Start mail input; end with <CRLF>.<CRLF>')
|
|
190
|
+
end
|
|
191
|
+
|
|
78
192
|
current_message[:source] ||= +""
|
|
79
193
|
|
|
80
194
|
lines.each do |line|
|
|
@@ -85,28 +199,124 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
85
199
|
end
|
|
86
200
|
|
|
87
201
|
def receive_message
|
|
88
|
-
|
|
202
|
+
message_size = current_message[:source].length
|
|
203
|
+
log_transcript('data', 'client', "Message complete (#{message_size} bytes)")
|
|
204
|
+
log_transcript('response', 'server', '250 OK: Message accepted')
|
|
205
|
+
|
|
206
|
+
begin
|
|
207
|
+
message_id = MailCatcher::Mail.add_message current_message
|
|
208
|
+
rescue => e
|
|
209
|
+
$stderr.puts "Error in add_message: #{e.message}"
|
|
210
|
+
$stderr.puts e.backtrace.join("\n")
|
|
211
|
+
message_id = nil
|
|
212
|
+
end
|
|
213
|
+
|
|
89
214
|
MailCatcher::Mail.delete_older_messages!
|
|
90
|
-
|
|
215
|
+
|
|
216
|
+
# Save transcript linked to message
|
|
217
|
+
save_transcript(message_id)
|
|
218
|
+
|
|
91
219
|
true
|
|
92
220
|
rescue => exception
|
|
221
|
+
log_transcript('error', 'server', "Exception: #{exception.class} - #{exception.message}")
|
|
93
222
|
MailCatcher.log_exception("Error receiving message", @current_message, exception)
|
|
223
|
+
|
|
224
|
+
# Save transcript even on error
|
|
225
|
+
save_transcript(nil)
|
|
226
|
+
|
|
94
227
|
false
|
|
95
228
|
ensure
|
|
96
229
|
@current_message = nil
|
|
230
|
+
@data_started = false
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Override to log EHLO/HELO command
|
|
234
|
+
def receive_ehlo_domain(domain)
|
|
235
|
+
log_transcript('command', 'client', "EHLO #{domain}")
|
|
236
|
+
|
|
237
|
+
# Call parent to handle the EHLO response
|
|
238
|
+
result = super
|
|
239
|
+
|
|
240
|
+
# Log the capabilities after they're sent
|
|
241
|
+
capabilities = get_server_capabilities
|
|
242
|
+
capabilities_str = capabilities.map { |cap| "250-#{cap}" }.join("\r\n")
|
|
243
|
+
# Replace first 250- with 250 followed by a space
|
|
244
|
+
if capabilities_str.start_with?("250-")
|
|
245
|
+
capabilities_str = "250 " + capabilities_str[4..-1]
|
|
246
|
+
end
|
|
247
|
+
log_transcript('response', 'server', capabilities_str)
|
|
248
|
+
|
|
249
|
+
result
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Override to log STARTTLS command and TLS negotiation
|
|
253
|
+
def process_starttls
|
|
254
|
+
log_transcript('tls', 'client', 'STARTTLS')
|
|
255
|
+
result = super
|
|
256
|
+
|
|
257
|
+
# Log TLS info after negotiation
|
|
258
|
+
if @tls_started
|
|
259
|
+
begin
|
|
260
|
+
protocol = get_cipher_protocol
|
|
261
|
+
cipher = get_cipher_name
|
|
262
|
+
log_transcript('tls', 'server', "TLS negotiation completed (#{protocol}, #{cipher})")
|
|
263
|
+
rescue
|
|
264
|
+
log_transcript('tls', 'server', "TLS negotiation completed")
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
result
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Override to log AUTH attempts (without logging credentials)
|
|
272
|
+
def receive_plain_auth(user, password)
|
|
273
|
+
log_transcript('command', 'client', "AUTH PLAIN [credentials hidden]")
|
|
274
|
+
result = super
|
|
275
|
+
|
|
276
|
+
if result
|
|
277
|
+
log_transcript('response', 'server', '235 2.7.0 Authentication successful')
|
|
278
|
+
else
|
|
279
|
+
log_transcript('response', 'server', '535 5.7.8 Authentication credentials invalid')
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
result
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Override to log unknown/invalid commands
|
|
286
|
+
def process_unknown(command, data)
|
|
287
|
+
log_transcript('command', 'client', "#{command} #{data}".strip)
|
|
288
|
+
result = super
|
|
289
|
+
|
|
290
|
+
log_transcript('response', 'server', '500 5.5.1 Command not recognized')
|
|
291
|
+
|
|
292
|
+
result
|
|
97
293
|
end
|
|
98
294
|
end
|
|
99
295
|
|
|
100
296
|
# Direct TLS (SMTPS) handler that starts TLS immediately on connection
|
|
101
297
|
class MailCatcher::SmtpTls < MailCatcher::Smtp
|
|
102
298
|
def post_init
|
|
103
|
-
# Increment connection count
|
|
299
|
+
# Increment connection count and set up connection info
|
|
104
300
|
super
|
|
105
301
|
|
|
302
|
+
# Log immediate TLS (SMTPS on port 1465)
|
|
303
|
+
log_transcript('tls', 'server', 'Immediate TLS enabled (SMTPS)')
|
|
304
|
+
|
|
106
305
|
# Start TLS immediately on connection for SMTPS (port 1465 behavior)
|
|
107
306
|
# The @@parms hash with starttls_options is already set by configure_smtp_ssl!
|
|
108
307
|
if defined?(@@parms) && @@parms[:starttls_options]
|
|
109
308
|
start_tls(@@parms[:starttls_options])
|
|
309
|
+
|
|
310
|
+
# Log TLS details after negotiation
|
|
311
|
+
if @tls_started
|
|
312
|
+
begin
|
|
313
|
+
protocol = get_cipher_protocol
|
|
314
|
+
cipher = get_cipher_name
|
|
315
|
+
log_transcript('tls', 'server', "TLS negotiation completed (#{protocol}, #{cipher})")
|
|
316
|
+
rescue
|
|
317
|
+
# TLS info not available yet
|
|
318
|
+
end
|
|
319
|
+
end
|
|
110
320
|
end
|
|
111
321
|
end
|
|
112
322
|
end
|
data/lib/mail_catcher/version.rb
CHANGED
|
@@ -161,7 +161,8 @@ module MailCatcher
|
|
|
161
161
|
"formats" => [
|
|
162
162
|
"source",
|
|
163
163
|
("html" if Mail.message_has_html? id),
|
|
164
|
-
("plain" if Mail.message_has_plain? id)
|
|
164
|
+
("plain" if Mail.message_has_plain? id),
|
|
165
|
+
("transcript" if Mail.message_transcript(id))
|
|
165
166
|
].compact,
|
|
166
167
|
"attachments" => Mail.message_attachments(id),
|
|
167
168
|
"bimi_location" => Mail.message_bimi_location(id),
|
|
@@ -169,7 +170,7 @@ module MailCatcher
|
|
|
169
170
|
"authentication_results" => Mail.message_authentication_results(id),
|
|
170
171
|
"encryption_data" => Mail.message_encryption_data(id),
|
|
171
172
|
"from_header" => Mail.message_from(id),
|
|
172
|
-
"to_header" => Mail.message_to(id)
|
|
173
|
+
"to_header" => Mail.message_to(id)
|
|
173
174
|
}))
|
|
174
175
|
else
|
|
175
176
|
not_found
|
|
@@ -222,6 +223,26 @@ module MailCatcher
|
|
|
222
223
|
end
|
|
223
224
|
end
|
|
224
225
|
|
|
226
|
+
get "/messages/:id/transcript.json" do
|
|
227
|
+
id = params[:id].to_i
|
|
228
|
+
if transcript = Mail.message_transcript(id)
|
|
229
|
+
content_type :json
|
|
230
|
+
JSON.generate(transcript)
|
|
231
|
+
else
|
|
232
|
+
not_found
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
get "/messages/:id.transcript" do
|
|
237
|
+
id = params[:id].to_i
|
|
238
|
+
if transcript = Mail.message_transcript(id)
|
|
239
|
+
content_type :html, charset: "utf-8"
|
|
240
|
+
erb :transcript, locals: { transcript: transcript }
|
|
241
|
+
else
|
|
242
|
+
not_found
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
225
246
|
get "/messages/:id/parts/:cid" do
|
|
226
247
|
id = params[:id].to_i
|
|
227
248
|
if part = Mail.message_part_cid(id, params[:cid])
|
|
@@ -19,4 +19,4 @@ var e=xe.createElement("input"),t=xe.createElement("select").appendChild(xe.crea
|
|
|
19
19
|
* Copyright 2013, Chris Hunt
|
|
20
20
|
* Released under the MIT license
|
|
21
21
|
*/
|
|
22
|
-
function(){function e(e){this.icon=e,this.opacity=.4,this.canvas=document.createElement("canvas"),this.font="Helvetica, Arial, sans-serif"}function t(e){return e=Math.round(e),isNaN(e)||e<1?"":e<10?" "+e:e>99?"99":e}function n(e,t,n,r,i){var o,a,s,u,l,c,d,f=document.getElementsByTagName("head")[0],p=document.createElement("link");p.rel="icon",a=11*(o=r.width/16),u=o,l=11*o,c=o,d=2*o,e.height=e.width=r.width,(s=e.getContext("2d")).font="bold "+a+"px "+n,i&&(s.globalAlpha=t),s.drawImage(r,0,0),s.globalAlpha=1,s.shadowColor="#FFF",s.shadowBlur=d,s.shadowOffsetX=0,s.shadowOffsetY=0,s.fillStyle="#FFF",s.fillText(i,u,l),s.fillText(i,u+c,l),s.fillText(i,u,l+c),s.fillText(i,u+c,l+c),s.fillStyle="#000",s.fillText(i,u+c/2,l+c/2),p.href=e.toDataURL("image/png"),f.removeChild(document.querySelector("link[rel=icon]")),f.appendChild(p)}e.prototype.set=function(e){var r=this,i=document.createElement("img");r.canvas.getContext&&(i.crossOrigin="anonymous",i.onload=function(){n(r.canvas,r.opacity,r.font,i,t(e))},i.src=this.icon)},this.Favcount=e}.call(this),function(){Favcount.VERSION="1.5.0"}.call(this),function(){var e,t=function(e,t){return function(){return e.apply(t,arguments)}};jQuery.expr.pseudos.icontains=function(e,t,n){var r,i;return(null!=(r=null!=(i=e.textContent)?i:e.innerText)?r:"").toUpperCase().indexOf(n[3].toUpperCase())>=0},e=function(){function e(){var e;this.nextTab=t(this.nextTab,this),this.previousTab=t(this.previousTab,this),this.openTab=t(this.openTab,this),this.selectedTab=t(this.selectedTab,this),this.getTab=t(this.getTab,this),$("#messages").on("click","tr",(e=this,function(t){return t.preventDefault(),e.loadMessage($(t.currentTarget).attr("data-message-id"))})),$("input[name=search]").on("keyup",function(e){return function(t){var n;return(n=$.trim($(t.currentTarget).val()))?e.searchMessages(n):e.clearSearch(),e.applyFilters()}}(this)),$("#searchClear").on("click",function(e){return function(t){return t.preventDefault(),$("input[name=search]").val("").focus(),e.clearSearch(),e.applyFilters()}}(this)),$("#attachmentFilter").on("change",function(e){return function(){return e.applyFilters()}}(this)),$("#message").on("click",".views .format.tab a",function(e){return function(t){return t.preventDefault(),e.loadMessageBody(e.selectedMessage(),$($(t.currentTarget).parent("li")).data("message-format"))}}(this)),$("#message iframe").on("load",function(e){return function(){return e.decorateMessageBody()}}(this)),$("#resizer").on("mousedown",function(e){return function(t){var n;return t.preventDefault(),n={mouseup:function(e){return e.preventDefault(),$(window).off(n)},mousemove:function(t){return t.preventDefault(),e.resizeTo(t.clientY)}},$(window).on(n)}}(this)),this.resizeToSaved(),$("nav.app .clear a").on("click",function(e){return function(t){if(t.preventDefault(),confirm("You will lose all your received messages.\n\nAre you sure you want to clear all messages?"))return $.ajax({url:new URL("messages",document.baseURI).toString(),type:"DELETE",success:function(){return e.clearMessages()},error:function(){return alert("Error while clearing all messages.")}})}}(this)),$("nav.app .quit a").on("click",function(e){return function(t){if(t.preventDefault(),confirm("You will lose all your received messages.\n\nAre you sure you want to quit?"))return e.quitting=!0,$.ajax({type:"DELETE",success:function(){return e.hasQuit()},error:function(){return e.quitting=!1,alert("Error while quitting.")}})}}(this)),this.favcount=new Favcount($('link[rel="icon"]').attr("href")),document.addEventListener("keydown",function(e){return function(t){var n;if("search"!==t.target.type){switch(t.code){case"ArrowUp":t.preventDefault(),e.selectedMessage()?e.loadMessage($("#messages tr.selected").prevAll(":visible").first().data("message-id")):e.loadMessage($("#messages tbody tr[data-message-id]").first().data("message-id"));break;case"ArrowDown":t.preventDefault(),e.selectedMessage()?e.loadMessage($("#messages tr.selected").nextAll(":visible").data("message-id")):e.loadMessage($("#messages tbody tr[data-message-id]:first").data("message-id"));break;case"ArrowLeft":t.preventDefault(),e.openTab(e.previousTab());break;case"ArrowRight":t.preventDefault(),e.openTab(e.nextTab());break;case"Backspace":case"Delete":t.preventDefault(),null!=(n=e.selectedMessage())&&$.ajax({url:new URL("messages/"+n,document.baseURI).toString(),type:"DELETE",success:function(){return e.removeMessage(n)},error:function(){return alert("Error while removing message.")}})}if(t.ctrlKey||t.metaKey)switch(t.code){case"ArrowUp":return t.preventDefault(),e.loadMessage($("#messages tbody tr[data-message-id]:visible").first().data("message-id"));case"ArrowDown":return t.preventDefault(),e.loadMessage($("#messages tbody tr[data-message-id]:visible").last().data("message-id"))}}}}(this)),this.refresh(),this.subscribe()}return e.prototype.parseDate=function(e){return"string"==typeof e?new Date(e):e},e.prototype.formatDate=function(e){var t;return"string"==typeof e&&(e=this.parseDate(e)),e?(t=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][e.getDay()]+", "+String(e.getDate()).padStart(2,"0")+" "+t[e.getMonth()]+" "+e.getFullYear()+" "+String(e.getHours()).padStart(2,"0")+":"+String(e.getMinutes()).padStart(2,"0")+":"+String(e.getSeconds()).padStart(2,"0")):null},e.prototype.formatSize=function(e){var t,n,r;return e?0===(e=parseInt(e))?"0 B":(n=1024,r=["B","KB","MB","GB"],t=Math.floor(Math.log(e)/Math.log(n)),(e/Math.pow(n,t)).toFixed(2).replace(/\.?0+$/,"")+" "+r[t]):"-"},e.prototype.messagesCount=function(){return $("#messages tr").length-1},e.prototype.updateMessagesCount=function(){return this.favcount.set(this.messagesCount()),document.title="MailCatcher ("+this.messagesCount()+")"},e.prototype.tabs=function(){return $("#message ul").children(".tab")},e.prototype.getTab=function(e){return $(this.tabs()[e])},e.prototype.selectedTab=function(){return this.tabs().index($("#message li.tab.selected"))},e.prototype.openTab=function(e){return this.getTab(e).children("a").click()},e.prototype.previousTab=function(e){var t;return(t=e||0===e?e:this.selectedTab()-1)<0&&(t=this.tabs().length-1),this.getTab(t).is(":visible")?t:this.previousTab(t-1)},e.prototype.nextTab=function(e){var t;return(t=e||this.selectedTab()+1)>this.tabs().length-1&&(t=0),this.getTab(t).is(":visible")?t:this.nextTab(t+1)},e.prototype.haveMessage=function(e){return null!=e.id&&(e=e.id),$('#messages tbody tr[data-message-id="'+e+'"]').length>0},e.prototype.selectedMessage=function(){return $("#messages tr.selected").data("message-id")},e.prototype.currentSearchQuery="",e.prototype.currentAttachmentFilter="all",e.prototype.searchMessages=function(e){return this.currentSearchQuery=e,this.applyFilters()},e.prototype.clearSearch=function(){return this.currentSearchQuery="",this.applyFilters()},e.prototype.applyFilters=function(){var e,t,n,r;return e=$("#messages tbody tr"),n=this.currentSearchQuery,t=$("#attachmentFilter").val(),this.currentAttachmentFilter=t,e.each((r=this,function(e,i){var o,a,s,u,l,c,d;if(o=$(i),l=!0,n&&(d=n.split(/\s+/),c=o.text().toUpperCase(),l=d.every(function(e){return c.indexOf(e.toUpperCase())>=0})),a=!0,(u=o.attr("data-message-id"))&&"all"!==t){if(void 0===(s=o.data("has-attachments")))return void $.getJSON("messages/"+u+".json",function(e){return o.data("has-attachments",e.attachments&&e.attachments.length>0),r.applyFilters()});"with"===t?a=s:"without"===t&&(a=!s)}return l&&a?o.show():o.hide()}))},e.prototype.formatSender=function(e){var t;return e?(t=(e=e.replace(/^<(.+?)>$/,"$1")).match(/^(.+?)\s+<(.+?)>$/))?t[1].trim()+" "+t[2].trim():e:""},e.prototype.parseSender=function(e){var t,n;return e?(n=(t=e.replace(/^<(.+?)>$/,"$1")).match(/^(.+?)\s+<(.+?)>$/))?{name:n[1].trim(),email:n[2].trim()}:{name:"",email:t}:{name:"",email:""}},e.prototype.getEmailPreview=function(e,t){var n;return this,e.formats&&(e.formats.includes("plain")||e.formats.includes("html"))?(n=e.formats.includes("plain")?"plain":"html",$.ajax({url:"messages/"+e.id+"."+n,type:"GET",success:function(e){var n;if((n=(n=e.replace(/<[^>]*>/g,"").trim()).substring(0,100)).length>=100&&(n+="..."),t)return t(n)},error:function(){if(t)return t("")}})):t?t(""):void 0},e.prototype.addMessage=function(e){var t,n,r,i,o,a,s,u,l,c,d,f,p,h,g,m,y,v,b,x;return this.formatSender(e.sender||"No sender"),y=e.attachments&&e.attachments.length>0,f=$("<td/>").addClass("subject-cell"),d=$("<div/>").addClass("subject-text").html("<strong>"+this.escapeHtml(e.subject||"No subject")+"</strong>"),a=$("<div/>").addClass("preview-text").text(""),f.append(d).append(a),i=$("<td/>").addClass("from-cell"),x=this.parseSender(e.sender||""),o=$("<div/>").addClass("sender-text-container"),x.name?(c=$("<div/>").addClass("sender-name").html("<strong>"+this.escapeHtml(x.name)+"</strong>"),l=$("<div/>").addClass("sender-email").text(x.email),o.append(c).append(l)):(l=$("<div/>").addClass("sender-email").text(x.email),o.append(l)),i.append(o).toggleClass("blank",!e.sender),t=$("<td/>").addClass("col-attachments"),y&&t.text("\ud83d\udcce"),n=$("<td/>").addClass("col-bimi"),(r=$("<svg/>").addClass("bimi-placeholder-icon").attr("viewBox","0 0 24 24").attr("fill","none").attr("stroke","currentColor").attr("stroke-width","2")).append($("<circle/>").attr("cx","12").attr("cy","12").attr("r","10")),r.append($("<text/>").attr("x","12").attr("y","13").attr("text-anchor","middle").attr("font-size","10").attr("font-weight","bold").text("B")),n.append(r),p=$("<td/>").addClass("to-cell"),e.recipients&&e.recipients.length>0?(m=e.recipients[0],v=this.parseSender(m),h=$("<div/>").addClass("sender-text-container"),v.name?(u=$("<div/>").addClass("sender-name").html("<strong>"+this.escapeHtml(v.name)+"</strong>"),s=$("<div/>").addClass("sender-email").text(v.email),h.append(u).append(s)):(s=$("<div/>").addClass("sender-email").text(v.email),h.append(s)),p.append(h)):p.addClass("blank").text("No recipients"),(g=$("<tr />").attr("data-message-id",e.id.toString()).append(t).append(n).append(i).append(p).append(f).append($("<td/>").text(this.formatDate(e.created_at))).append($("<td/>").text(this.formatSize(e.size)))).data("has-attachments",y),g.prependTo($("#messages tbody")),b=this,$.getJSON("messages/"+e.id+".json",function(e){var r,i,l,c,d;if(e.from_header&&(o.empty(),(c=b.parseSender(e.from_header)).name?(l=$("<div/>").addClass("sender-name").html("<strong>"+b.escapeHtml(c.name)+"</strong>"),i=$("<div/>").addClass("sender-email").text(c.email),o.append(l).append(i)):(i=$("<div/>").addClass("sender-email").text(c.email),o.append(i))),e.to_header&&(p.empty(),(d=e.to_header.split(",").map(function(e){return e.trim()})).length>0&&(m=d[0],v=b.parseSender(m),h=$("<div/>").addClass("sender-text-container"),v.name?(u=$("<div/>").addClass("sender-name").html("<strong>"+b.escapeHtml(v.name)+"</strong>"),s=$("<div/>").addClass("sender-email").text(v.email),h.append(u).append(s)):(s=$("<div/>").addClass("sender-email").text(v.email),h.append(s)),p.append(h))),e.attachments&&e.attachments.length>0&&(g.data("has-attachments",!0),t.text("\ud83d\udcce")),e.preview_text?a.text(e.preview_text):b.getEmailPreview(e,function(e){return a.text(e)}),e.bimi_location)return n.empty(),r=$("<img/>").addClass("bimi-image").attr("src",e.bimi_location).attr("alt","BIMI"),n.append(r)}),this.updateMessagesCount(),this.applyFilters()},e.prototype.escapeHtml=function(e){var t;return(t=document.createElement("div")).textContent=e,t.innerHTML},e.prototype.removeMessage=function(e){var t,n,r;return(t=(n=$('#messages tbody tr[data-message-id="'+e+'"]')).is(".selected"))&&(r=n.next().data("message-id")||n.prev().data("message-id")),n.remove(),t&&(r?this.loadMessage(r):this.unselectMessage()),this.updateMessagesCount()},e.prototype.clearMessages=function(){return $("#messages tbody tr").remove(),this.unselectMessage(),this.updateMessagesCount()},e.prototype.scrollToRow=function(e){var t,n;return(n=e.offset().top-$("#messages").offset().top)<0?$("#messages").scrollTop($("#messages").scrollTop()+n-20):(t=n+e.height()-$("#messages").height())>0?$("#messages").scrollTop($("#messages").scrollTop()+t+20):void 0},e.prototype.unselectMessage=function(){return $("#messages tbody, #message .metadata dd").empty(),$(".attachments-list").empty(),$(".attachments-column").removeClass("visible"),$("#message iframe").attr("src","about:blank"),null},e.prototype.cleanEmailAddress=function(e){return e?e.replace(/^<(.+)>$/,"$1"):e},e.prototype.loadMessage=function(e){var t,n;if(null!=(null!=e?e.id:void 0)&&(e=e.id),e||(e=$("#messages tr.selected").attr("data-message-id")),null!=e)return $("#messages tbody tr:not([data-message-id='"+e+"'])").removeClass("selected"),(t=$("#messages tbody tr[data-message-id='"+e+"']")).addClass("selected"),this.scrollToRow(t),$.getJSON("messages/"+e+".json",(n=this,function(r){var i,o,a;return(t=$("#messages tbody tr[data-message-id='"+e+"']")).data("has-attachments",r.attachments&&r.attachments.length>0),$("#message .metadata dd.created_at").text(n.formatDate(r.created_at)),r.from_header?$("#message .metadata dd.from").text(n.formatSender(r.from_header)):$("#message .metadata dd.from").text(n.cleanEmailAddress(r.sender)),r.to_header?(a=r.to_header.split(",").map(function(e){return n.formatSender(e.trim())}).join(", "),$("#message .metadata dd.to").text(a)):$("#message .metadata dd.to").text((r.recipients||[]).map(function(e){return n.cleanEmailAddress(e)}).join(", ")),$("#message .metadata dd.subject").text(r.subject),$("#message .views .tab.format").each(function(t,n){var i,o;return o=(i=$(n)).attr("data-message-format"),$.inArray(o,r.formats)>=0?(i.find("a").attr("href","messages/"+e+"."+o),i.show()):i.hide()}),$("#message .views .tab.selected:not(:visible)").length&&($("#message .views .tab.selected").removeClass("selected"),$("#message .views .tab:visible:first").addClass("selected")),r.attachments.length?(i=$(".attachments-list").empty(),o=n,$.each(r.attachments,function(t,n){var r,a,s;return a=$("<li/>"),r=$("<a/>").attr("href","messages/"+e+"/parts/"+n.cid).addClass(n.type.split("/",1)[0]).addClass(n.type.replace("/","-")).text(n.filename),(s=$("<div/>").addClass("attachment-meta")).append($("<div/>").addClass("attachment-size").text(o.formatSize(n.size))),s.append($("<div/>").addClass("attachment-type").text(n.type)),a.append(r).append(s),i.append(a)}),$(".attachments-column").addClass("visible")):$(".attachments-column").removeClass("visible"),$("#message .views .download a").attr("href","messages/"+e+".eml"),n.loadMessageBody()}))},e.prototype.loadMessageBody=function(e,t){if(e||(e=this.selectedMessage()),t||(t=$("#message .views .tab.format.selected").attr("data-message-format")),t||(t="html"),$('#message .views .tab[data-message-format="'+t+'"]:not(.selected)').addClass("selected"),$('#message .views .tab:not([data-message-format="'+t+'"]).selected').removeClass("selected"),null!=e)return $("#message iframe").attr("src","messages/"+e+"."+t)},e.prototype.decorateMessageBody=function(){var e,t,n,r,i;switch($("#message .views .tab.format.selected").attr("data-message-format")){case"html":return e=$("#message iframe").contents().find("body"),$("a",e).attr("target","_blank");case"plain":return(e=(n=$("#message iframe").contents()).find("body")).length?(e.css("font-family","sans-serif"),e.css("white-space","pre-wrap"),e.css("word-wrap","break-word")):(i=(i=(i=(i=(i=(i=n.text()).replace(/&/g,"&")).replace(/</g,"<")).replace(/>/g,">")).replace(/"/g,""")).replace(/((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@?^=%&\/~\+#])?)/g,'<a href="$1" target="_blank">$1</a>'),n.find("html").html('<body style="font-family: sans-serif; white-space: pre-wrap; word-wrap: break-word">'+i+"</body>"));case"source":if((e=(n=$("#message iframe").contents()).find("body")).length&&(t=e.text()))return r="<body style=\"background: #f5f5f5; color: #1a1a1a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; padding: 0; margin: 0; line-height: 1.6;\">\n <div style=\"padding: 20px 28px;\">\n "+('<pre><code class="language-xml">'+t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")+"</code></pre>")+"\n </div>\n</body>",n.find("html").html(r),setTimeout(function(){return n.find("code").each(function(e,t){return hljs.highlightElement(t)})},0)}},e.prototype.refresh=function(){return $.getJSON("messages",(e=this,function(t){return $.each(t,function(t,n){if(!e.haveMessage(n))return e.addMessage(n)}),e.updateMessagesCount()}));var e},e.prototype.subscribe=function(){return"undefined"!=typeof WebSocket&&null!==WebSocket?this.subscribeWebSocket():this.subscribePoll()},e.prototype.reconnectWebSocketAttempts=0,e.prototype.maxReconnectAttempts=10,e.prototype.reconnectBaseDelay=1e3,e.prototype.subscribeWebSocket=function(){var e,t,n;return e="https:"===window.location.protocol,(t=new URL("messages",document.baseURI)).protocol=e?"wss":"ws",this.websocket=new WebSocket(t.toString()),this.websocket.onopen=(n=this,function(){return console.log("[MailCatcher] WebSocket connection established"),n.reconnectWebSocketAttempts=0,n.updateWebSocketStatus(!0)}),this.websocket.onmessage=function(e){return function(t){var n,r;try{if(n=JSON.parse(t.data),console.log("[MailCatcher] WebSocket message received:",n),"add"===n.type)return e.addMessage(n.message);if("remove"===n.type)return e.removeMessage(n.id);if("clear"===n.type)return e.clearMessages();if("quit"===n.type&&!e.quitting)return e.hasQuit()}catch(i){return r=i,console.error("[MailCatcher] Error processing WebSocket message:",r)}}}(this),this.websocket.onerror=function(e){return function(t){return console.error("[MailCatcher] WebSocket error:",t),e.updateWebSocketStatus(!1)}}(this),this.websocket.onclose=function(e){return function(){return console.log("[MailCatcher] WebSocket connection closed"),e.updateWebSocketStatus(!1),e.attemptWebSocketReconnect()}}(this)},e.prototype.subscribePoll=function(){if(null==this.refreshInterval)return this.refreshInterval=setInterval((e=this,function(){return e.refresh()}),1e3);var e},e.prototype.attemptWebSocketReconnect=function(){var e,t;return this.reconnectWebSocketAttempts<this.maxReconnectAttempts?(e=this.reconnectBaseDelay*Math.pow(2,this.reconnectWebSocketAttempts),this.reconnectWebSocketAttempts++,console.log("[MailCatcher] Attempting WebSocket reconnection in "+e+"ms (attempt "+this.reconnectWebSocketAttempts+"/"+this.maxReconnectAttempts+")"),setTimeout((t=this,function(){return t.subscribeWebSocket()}),e)):(console.log("[MailCatcher] Max WebSocket reconnection attempts reached, staying in polling mode"),this.subscribePoll())},e.prototype.resizeToSavedKey="mailcatcherSeparatorHeight",e.prototype.resizeTo=function(e){var t;return $("#messages").css({height:e-$("#messages").offset().top}),null!=(t=window.localStorage)?t.setItem(this.resizeToSavedKey,e):void 0},e.prototype.resizeToSaved=function(){var e,t;if(e=parseInt(null!=(t=window.localStorage)?t.getItem(this.resizeToSavedKey):void 0),!isNaN(e))return this.resizeTo(e)},e.prototype.updateWebSocketStatus=function(e){var t,n;if(t=document.getElementById("websocketStatus"),n=document.getElementById("statusText"),t&&n)return e?(t.classList.remove("disconnected"),n.textContent="Connected"):(t.classList.add("disconnected"),n.textContent="Disconnected")},e.prototype.hasQuit=function(){return console.log("[MailCatcher] Server has quit")},e}(),$(function(){return window.MailCatcher=new e})}.call(this);
|
|
22
|
+
function(){function e(e){this.icon=e,this.opacity=.4,this.canvas=document.createElement("canvas"),this.font="Helvetica, Arial, sans-serif"}function t(e){return e=Math.round(e),isNaN(e)||e<1?"":e<10?" "+e:e>99?"99":e}function n(e,t,n,r,i){var o,a,s,u,l,c,d,f=document.getElementsByTagName("head")[0],p=document.createElement("link");p.rel="icon",a=11*(o=r.width/16),u=o,l=11*o,c=o,d=2*o,e.height=e.width=r.width,(s=e.getContext("2d")).font="bold "+a+"px "+n,i&&(s.globalAlpha=t),s.drawImage(r,0,0),s.globalAlpha=1,s.shadowColor="#FFF",s.shadowBlur=d,s.shadowOffsetX=0,s.shadowOffsetY=0,s.fillStyle="#FFF",s.fillText(i,u,l),s.fillText(i,u+c,l),s.fillText(i,u,l+c),s.fillText(i,u+c,l+c),s.fillStyle="#000",s.fillText(i,u+c/2,l+c/2),p.href=e.toDataURL("image/png"),f.removeChild(document.querySelector("link[rel=icon]")),f.appendChild(p)}e.prototype.set=function(e){var r=this,i=document.createElement("img");r.canvas.getContext&&(i.crossOrigin="anonymous",i.onload=function(){n(r.canvas,r.opacity,r.font,i,t(e))},i.src=this.icon)},this.Favcount=e}.call(this),function(){Favcount.VERSION="1.5.0"}.call(this),function(){var e,t=function(e,t){return function(){return e.apply(t,arguments)}};jQuery.expr.pseudos.icontains=function(e,t,n){var r,i;return(null!=(r=null!=(i=e.textContent)?i:e.innerText)?r:"").toUpperCase().indexOf(n[3].toUpperCase())>=0},e=function(){function e(){var e;this.nextTab=t(this.nextTab,this),this.previousTab=t(this.previousTab,this),this.openTab=t(this.openTab,this),this.selectedTab=t(this.selectedTab,this),this.getTab=t(this.getTab,this),$("#messages").on("click","tr",(e=this,function(t){return t.preventDefault(),e.loadMessage($(t.currentTarget).attr("data-message-id"))})),$("input[name=search]").on("keyup",function(e){return function(t){var n;return(n=$.trim($(t.currentTarget).val()))?e.searchMessages(n):e.clearSearch(),e.applyFilters()}}(this)),$("#searchClear").on("click",function(e){return function(t){return t.preventDefault(),$("input[name=search]").val("").focus(),e.clearSearch(),e.applyFilters()}}(this)),$("#attachmentFilter").on("change",function(e){return function(){return e.applyFilters()}}(this)),$("#message").on("click",".views .format.tab a",function(e){return function(t){return t.preventDefault(),e.loadMessageBody(e.selectedMessage(),$($(t.currentTarget).parent("li")).data("message-format"))}}(this)),$("#message iframe").on("load",function(e){return function(){return e.decorateMessageBody()}}(this)),$("#resizer").on("mousedown",function(e){return function(t){var n;return t.preventDefault(),n={mouseup:function(e){return e.preventDefault(),$(window).off(n)},mousemove:function(t){return t.preventDefault(),e.resizeTo(t.clientY)}},$(window).on(n)}}(this)),this.resizeToSaved(),$("nav.app .clear a").on("click",function(e){return function(t){if(t.preventDefault(),confirm("You will lose all your received messages.\n\nAre you sure you want to clear all messages?"))return $.ajax({url:new URL("messages",document.baseURI).toString(),type:"DELETE",success:function(){return e.clearMessages()},error:function(){return alert("Error while clearing all messages.")}})}}(this)),$("nav.app .quit a").on("click",function(e){return function(t){if(t.preventDefault(),confirm("You will lose all your received messages.\n\nAre you sure you want to quit?"))return e.quitting=!0,$.ajax({type:"DELETE",success:function(){return e.hasQuit()},error:function(){return e.quitting=!1,alert("Error while quitting.")}})}}(this)),this.favcount=new Favcount($('link[rel="icon"]').attr("href")),document.addEventListener("keydown",function(e){return function(t){var n;if("search"!==t.target.type){switch(t.code){case"ArrowUp":t.preventDefault(),e.selectedMessage()?e.loadMessage($("#messages tr.selected").prevAll(":visible").first().data("message-id")):e.loadMessage($("#messages tbody tr[data-message-id]").first().data("message-id"));break;case"ArrowDown":t.preventDefault(),e.selectedMessage()?e.loadMessage($("#messages tr.selected").nextAll(":visible").data("message-id")):e.loadMessage($("#messages tbody tr[data-message-id]:first").data("message-id"));break;case"ArrowLeft":t.preventDefault(),e.openTab(e.previousTab());break;case"ArrowRight":t.preventDefault(),e.openTab(e.nextTab());break;case"Backspace":case"Delete":t.preventDefault(),null!=(n=e.selectedMessage())&&$.ajax({url:new URL("messages/"+n,document.baseURI).toString(),type:"DELETE",success:function(){return e.removeMessage(n)},error:function(){return alert("Error while removing message.")}})}if(t.ctrlKey||t.metaKey)switch(t.code){case"ArrowUp":return t.preventDefault(),e.loadMessage($("#messages tbody tr[data-message-id]:visible").first().data("message-id"));case"ArrowDown":return t.preventDefault(),e.loadMessage($("#messages tbody tr[data-message-id]:visible").last().data("message-id"))}}}}(this)),this.refresh(),this.subscribe()}return e.prototype.parseDate=function(e){return"string"==typeof e?new Date(e):e},e.prototype.formatDate=function(e){var t;return"string"==typeof e&&(e=this.parseDate(e)),e?(t=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][e.getDay()]+", "+String(e.getDate()).padStart(2,"0")+" "+t[e.getMonth()]+" "+e.getFullYear()+" "+String(e.getHours()).padStart(2,"0")+":"+String(e.getMinutes()).padStart(2,"0")+":"+String(e.getSeconds()).padStart(2,"0")):null},e.prototype.formatSize=function(e){var t,n,r;return e?0===(e=parseInt(e))?"0 B":(n=1024,r=["B","KB","MB","GB"],t=Math.floor(Math.log(e)/Math.log(n)),(e/Math.pow(n,t)).toFixed(2).replace(/\.?0+$/,"")+" "+r[t]):"-"},e.prototype.messagesCount=function(){return $("#messages tr").length-1},e.prototype.updateMessagesCount=function(){return this.favcount.set(this.messagesCount()),document.title="MailCatcher ("+this.messagesCount()+")"},e.prototype.tabs=function(){return $("#message ul").children(".tab")},e.prototype.getTab=function(e){return $(this.tabs()[e])},e.prototype.selectedTab=function(){return this.tabs().index($("#message li.tab.selected"))},e.prototype.openTab=function(e){return this.getTab(e).children("a").click()},e.prototype.previousTab=function(e){var t;return(t=e||0===e?e:this.selectedTab()-1)<0&&(t=this.tabs().length-1),this.getTab(t).is(":visible")?t:this.previousTab(t-1)},e.prototype.nextTab=function(e){var t;return(t=e||this.selectedTab()+1)>this.tabs().length-1&&(t=0),this.getTab(t).is(":visible")?t:this.nextTab(t+1)},e.prototype.haveMessage=function(e){return null!=e.id&&(e=e.id),$('#messages tbody tr[data-message-id="'+e+'"]').length>0},e.prototype.selectedMessage=function(){return $("#messages tr.selected").data("message-id")},e.prototype.currentSearchQuery="",e.prototype.currentAttachmentFilter="all",e.prototype.searchMessages=function(e){return this.currentSearchQuery=e,this.applyFilters()},e.prototype.clearSearch=function(){return this.currentSearchQuery="",this.applyFilters()},e.prototype.applyFilters=function(){var e,t,n,r;return e=$("#messages tbody tr"),n=this.currentSearchQuery,t=$("#attachmentFilter").val(),this.currentAttachmentFilter=t,e.each((r=this,function(e,i){var o,a,s,u,l,c,d;if(o=$(i),l=!0,n&&(d=n.split(/\s+/),c=o.text().toUpperCase(),l=d.every(function(e){return c.indexOf(e.toUpperCase())>=0})),a=!0,(u=o.attr("data-message-id"))&&"all"!==t){if(void 0===(s=o.data("has-attachments")))return void $.getJSON("messages/"+u+".json",function(e){return o.data("has-attachments",e.attachments&&e.attachments.length>0),r.applyFilters()});"with"===t?a=s:"without"===t&&(a=!s)}return l&&a?o.show():o.hide()}))},e.prototype.formatSender=function(e){var t;return e?(t=(e=e.replace(/^<(.+?)>$/,"$1")).match(/^(.+?)\s+<(.+?)>$/))?t[1].trim()+" "+t[2].trim():e:""},e.prototype.parseSender=function(e){var t,n;return e?(n=(t=e.replace(/^<(.+?)>$/,"$1")).match(/^(.+?)\s+<(.+?)>$/))?{name:n[1].trim(),email:n[2].trim()}:{name:"",email:t}:{name:"",email:""}},e.prototype.getEmailPreview=function(e,t){var n;return this,e.formats&&(e.formats.includes("plain")||e.formats.includes("html"))?(n=e.formats.includes("plain")?"plain":"html",$.ajax({url:"messages/"+e.id+"."+n,type:"GET",success:function(e){var n;if((n=(n=e.replace(/<[^>]*>/g,"").trim()).substring(0,100)).length>=100&&(n+="..."),t)return t(n)},error:function(){if(t)return t("")}})):t?t(""):void 0},e.prototype.addMessage=function(e){var t,n,r,i,o,a,s,u,l,c,d,f,p,h,g,m,y,v,b,x;return this.formatSender(e.sender||"No sender"),y=e.attachments&&e.attachments.length>0,f=$("<td/>").addClass("subject-cell"),d=$("<div/>").addClass("subject-text").html("<strong>"+this.escapeHtml(e.subject||"No subject")+"</strong>"),a=$("<div/>").addClass("preview-text").text(""),f.append(d).append(a),i=$("<td/>").addClass("from-cell"),x=this.parseSender(e.sender||""),o=$("<div/>").addClass("sender-text-container"),x.name?(c=$("<div/>").addClass("sender-name").html("<strong>"+this.escapeHtml(x.name)+"</strong>"),l=$("<div/>").addClass("sender-email").text(x.email),o.append(c).append(l)):(l=$("<div/>").addClass("sender-email").text(x.email),o.append(l)),i.append(o).toggleClass("blank",!e.sender),t=$("<td/>").addClass("col-attachments"),y&&t.text("\ud83d\udcce"),n=$("<td/>").addClass("col-bimi"),(r=$("<svg/>").addClass("bimi-placeholder-icon").attr("viewBox","0 0 24 24").attr("fill","none").attr("stroke","currentColor").attr("stroke-width","2")).append($("<circle/>").attr("cx","12").attr("cy","12").attr("r","10")),r.append($("<text/>").attr("x","12").attr("y","13").attr("text-anchor","middle").attr("font-size","10").attr("font-weight","bold").text("B")),n.append(r),p=$("<td/>").addClass("to-cell"),e.recipients&&e.recipients.length>0?(m=e.recipients[0],v=this.parseSender(m),h=$("<div/>").addClass("sender-text-container"),v.name?(u=$("<div/>").addClass("sender-name").html("<strong>"+this.escapeHtml(v.name)+"</strong>"),s=$("<div/>").addClass("sender-email").text(v.email),h.append(u).append(s)):(s=$("<div/>").addClass("sender-email").text(v.email),h.append(s)),p.append(h)):p.addClass("blank").text("No recipients"),(g=$("<tr />").attr("data-message-id",e.id.toString()).append(t).append(n).append(i).append(p).append(f).append($("<td/>").text(this.formatDate(e.created_at))).append($("<td/>").text(this.formatSize(e.size)))).data("has-attachments",y),g.prependTo($("#messages tbody")),b=this,$.getJSON("messages/"+e.id+".json",function(e){var r,i,l,c,d;if(e.from_header&&(o.empty(),(c=b.parseSender(e.from_header)).name?(l=$("<div/>").addClass("sender-name").html("<strong>"+b.escapeHtml(c.name)+"</strong>"),i=$("<div/>").addClass("sender-email").text(c.email),o.append(l).append(i)):(i=$("<div/>").addClass("sender-email").text(c.email),o.append(i))),e.to_header&&(p.empty(),(d=e.to_header.split(",").map(function(e){return e.trim()})).length>0&&(m=d[0],v=b.parseSender(m),h=$("<div/>").addClass("sender-text-container"),v.name?(u=$("<div/>").addClass("sender-name").html("<strong>"+b.escapeHtml(v.name)+"</strong>"),s=$("<div/>").addClass("sender-email").text(v.email),h.append(u).append(s)):(s=$("<div/>").addClass("sender-email").text(v.email),h.append(s)),p.append(h))),e.attachments&&e.attachments.length>0&&(g.data("has-attachments",!0),t.text("\ud83d\udcce")),e.preview_text?a.text(e.preview_text):b.getEmailPreview(e,function(e){return a.text(e)}),e.bimi_location)return n.empty(),r=$("<img/>").addClass("bimi-image").attr("src",e.bimi_location).attr("alt","BIMI"),n.append(r)}),this.updateMessagesCount(),this.applyFilters()},e.prototype.escapeHtml=function(e){var t;return(t=document.createElement("div")).textContent=e,t.innerHTML},e.prototype.removeMessage=function(e){var t,n,r;return(t=(n=$('#messages tbody tr[data-message-id="'+e+'"]')).is(".selected"))&&(r=n.next().data("message-id")||n.prev().data("message-id")),n.remove(),t&&(r?this.loadMessage(r):this.unselectMessage()),this.updateMessagesCount()},e.prototype.clearMessages=function(){return $("#messages tbody tr").remove(),this.unselectMessage(),this.updateMessagesCount()},e.prototype.scrollToRow=function(e){var t,n;return(n=e.offset().top-$("#messages").offset().top)<0?$("#messages").scrollTop($("#messages").scrollTop()+n-20):(t=n+e.height()-$("#messages").height())>0?$("#messages").scrollTop($("#messages").scrollTop()+t+20):void 0},e.prototype.unselectMessage=function(){return $("#messages tbody, #message .metadata dd").empty(),$(".attachments-list").empty(),$(".attachments-column").removeClass("visible"),$("#message iframe").attr("src","about:blank"),null},e.prototype.cleanEmailAddress=function(e){return e?e.replace(/^<(.+)>$/,"$1"):e},e.prototype.loadMessage=function(e){var t,n;if(null!=(null!=e?e.id:void 0)&&(e=e.id),e||(e=$("#messages tr.selected").attr("data-message-id")),null!=e)return $("#messages tbody tr:not([data-message-id='"+e+"'])").removeClass("selected"),(t=$("#messages tbody tr[data-message-id='"+e+"']")).addClass("selected"),this.scrollToRow(t),$.getJSON("messages/"+e+".json",(n=this,function(r){var i,o,a;return(t=$("#messages tbody tr[data-message-id='"+e+"']")).data("has-attachments",r.attachments&&r.attachments.length>0),$("#message .metadata dd.created_at").text(n.formatDate(r.created_at)),r.from_header?$("#message .metadata dd.from").text(n.formatSender(r.from_header)):$("#message .metadata dd.from").text(n.cleanEmailAddress(r.sender)),r.to_header?(a=r.to_header.split(",").map(function(e){return n.formatSender(e.trim())}).join(", "),$("#message .metadata dd.to").text(a)):$("#message .metadata dd.to").text((r.recipients||[]).map(function(e){return n.cleanEmailAddress(e)}).join(", ")),$("#message .metadata dd.subject").text(r.subject),$("#message .views .tab.format").each(function(t,n){var i,o;return"transcript"===(o=(i=$(n)).attr("data-message-format"))?i.show():$.inArray(o,r.formats)>=0?(i.find("a").attr("href","messages/"+e+"."+o),i.show()):i.hide()}),$("#message .views .tab.selected:not(:visible)").length&&($("#message .views .tab.selected").removeClass("selected"),$("#message .views .tab:visible:first").addClass("selected")),r.attachments.length?(i=$(".attachments-list").empty(),o=n,$.each(r.attachments,function(t,n){var r,a,s;return a=$("<li/>"),r=$("<a/>").attr("href","messages/"+e+"/parts/"+n.cid).addClass(n.type.split("/",1)[0]).addClass(n.type.replace("/","-")).text(n.filename),(s=$("<div/>").addClass("attachment-meta")).append($("<div/>").addClass("attachment-size").text(o.formatSize(n.size))),s.append($("<div/>").addClass("attachment-type").text(n.type)),a.append(r).append(s),i.append(a)}),$(".attachments-column").addClass("visible")):$(".attachments-column").removeClass("visible"),$("#message .views .download a").attr("href","messages/"+e+".eml"),n.loadMessageBody()}))},e.prototype.loadMessageBody=function(e,t){if(e||(e=this.selectedMessage()),t||(t=$("#message .views .tab.format.selected").attr("data-message-format")),t||(t="html"),$('#message .views .tab[data-message-format="'+t+'"]:not(.selected)').addClass("selected"),$('#message .views .tab:not([data-message-format="'+t+'"]).selected').removeClass("selected"),null!=e)return $("#message iframe").attr("src","messages/"+e+"."+t)},e.prototype.decorateMessageBody=function(){var e,t,n,r,i;switch($("#message .views .tab.format.selected").attr("data-message-format")){case"html":return e=$("#message iframe").contents().find("body"),$("a",e).attr("target","_blank");case"plain":return(e=(n=$("#message iframe").contents()).find("body")).length?(e.css("font-family","sans-serif"),e.css("white-space","pre-wrap"),e.css("word-wrap","break-word")):(i=(i=(i=(i=(i=(i=n.text()).replace(/&/g,"&")).replace(/</g,"<")).replace(/>/g,">")).replace(/"/g,""")).replace(/((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@?^=%&\/~\+#])?)/g,'<a href="$1" target="_blank">$1</a>'),n.find("html").html('<body style="font-family: sans-serif; white-space: pre-wrap; word-wrap: break-word">'+i+"</body>"));case"source":if((e=(n=$("#message iframe").contents()).find("body")).length&&(t=e.text()))return r="<body style=\"background: #f5f5f5; color: #1a1a1a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; padding: 0; margin: 0; line-height: 1.6;\">\n <div style=\"padding: 20px 28px;\">\n "+('<pre><code class="language-xml">'+t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")+"</code></pre>")+"\n </div>\n</body>",n.find("html").html(r),setTimeout(function(){return n.find("code").each(function(e,t){return hljs.highlightElement(t)})},0)}},e.prototype.refresh=function(){return $.getJSON("messages",(e=this,function(t){return $.each(t,function(t,n){if(!e.haveMessage(n))return e.addMessage(n)}),e.updateMessagesCount()}));var e},e.prototype.subscribe=function(){return"undefined"!=typeof WebSocket&&null!==WebSocket?this.subscribeWebSocket():this.subscribePoll()},e.prototype.reconnectWebSocketAttempts=0,e.prototype.maxReconnectAttempts=10,e.prototype.reconnectBaseDelay=1e3,e.prototype.subscribeWebSocket=function(){var e,t,n;return e="https:"===window.location.protocol,(t=new URL("messages",document.baseURI)).protocol=e?"wss":"ws",this.websocket=new WebSocket(t.toString()),this.websocket.onopen=(n=this,function(){return console.log("[MailCatcher] WebSocket connection established"),n.reconnectWebSocketAttempts=0,n.updateWebSocketStatus(!0)}),this.websocket.onmessage=function(e){return function(t){var n,r;try{if(n=JSON.parse(t.data),console.log("[MailCatcher] WebSocket message received:",n),"add"===n.type)return e.addMessage(n.message);if("remove"===n.type)return e.removeMessage(n.id);if("clear"===n.type)return e.clearMessages();if("quit"===n.type&&!e.quitting)return e.hasQuit()}catch(i){return r=i,console.error("[MailCatcher] Error processing WebSocket message:",r)}}}(this),this.websocket.onerror=function(e){return function(t){return console.error("[MailCatcher] WebSocket error:",t),e.updateWebSocketStatus(!1)}}(this),this.websocket.onclose=function(e){return function(){return console.log("[MailCatcher] WebSocket connection closed"),e.updateWebSocketStatus(!1),e.attemptWebSocketReconnect()}}(this)},e.prototype.subscribePoll=function(){if(null==this.refreshInterval)return this.refreshInterval=setInterval((e=this,function(){return e.refresh()}),1e3);var e},e.prototype.attemptWebSocketReconnect=function(){var e,t;return this.reconnectWebSocketAttempts<this.maxReconnectAttempts?(e=this.reconnectBaseDelay*Math.pow(2,this.reconnectWebSocketAttempts),this.reconnectWebSocketAttempts++,console.log("[MailCatcher] Attempting WebSocket reconnection in "+e+"ms (attempt "+this.reconnectWebSocketAttempts+"/"+this.maxReconnectAttempts+")"),setTimeout((t=this,function(){return t.subscribeWebSocket()}),e)):(console.log("[MailCatcher] Max WebSocket reconnection attempts reached, staying in polling mode"),this.subscribePoll())},e.prototype.resizeToSavedKey="mailcatcherSeparatorHeight",e.prototype.resizeTo=function(e){var t;return $("#messages").css({height:e-$("#messages").offset().top}),null!=(t=window.localStorage)?t.setItem(this.resizeToSavedKey,e):void 0},e.prototype.resizeToSaved=function(){var e,t;if(e=parseInt(null!=(t=window.localStorage)?t.getItem(this.resizeToSavedKey):void 0),!isNaN(e))return this.resizeTo(e)},e.prototype.updateWebSocketStatus=function(e){var t,n;if(t=document.getElementById("websocketStatus"),n=document.getElementById("statusText"),t&&n)return e?(t.classList.remove("disconnected"),n.textContent="Connected"):(t.classList.add("disconnected"),n.textContent="Disconnected")},e.prototype.hasQuit=function(){return console.log("[MailCatcher] Server has quit")},e}(),$(function(){return window.MailCatcher=new e})}.call(this);
|
data/views/index.erb
CHANGED
|
@@ -894,6 +894,198 @@
|
|
|
894
894
|
height: 18px;
|
|
895
895
|
}
|
|
896
896
|
|
|
897
|
+
/* SMTP Transcript Styles */
|
|
898
|
+
#message iframe.body[src*=".transcript"] {
|
|
899
|
+
background: #ffffff;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.transcript-container {
|
|
903
|
+
padding: 20px 28px;
|
|
904
|
+
font-family: 'Monaco', 'Courier New', 'Consolas', monospace;
|
|
905
|
+
font-size: 12px;
|
|
906
|
+
line-height: 1.6;
|
|
907
|
+
background: #ffffff;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.transcript-header {
|
|
911
|
+
background: #f9f9f9;
|
|
912
|
+
border: 1px solid #e8eaed;
|
|
913
|
+
border-radius: 8px;
|
|
914
|
+
padding: 16px 20px;
|
|
915
|
+
margin-bottom: 20px;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.transcript-header h3 {
|
|
919
|
+
font-size: 12px;
|
|
920
|
+
font-weight: 600;
|
|
921
|
+
text-transform: uppercase;
|
|
922
|
+
letter-spacing: 0.5px;
|
|
923
|
+
color: #5f5f5f;
|
|
924
|
+
margin: 0 0 12px 0;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
.transcript-info-grid {
|
|
928
|
+
display: grid;
|
|
929
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
930
|
+
gap: 12px;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.transcript-info-item {
|
|
934
|
+
display: flex;
|
|
935
|
+
flex-direction: column;
|
|
936
|
+
gap: 4px;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.transcript-info-label {
|
|
940
|
+
font-size: 10px;
|
|
941
|
+
font-weight: 600;
|
|
942
|
+
color: #999;
|
|
943
|
+
text-transform: uppercase;
|
|
944
|
+
letter-spacing: 0.5px;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.transcript-info-value {
|
|
948
|
+
font-size: 12px;
|
|
949
|
+
color: #1a1a1a;
|
|
950
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
951
|
+
word-break: break-all;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.transcript-info-value.success {
|
|
955
|
+
color: #34a853;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
.transcript-search-box {
|
|
959
|
+
margin-bottom: 16px;
|
|
960
|
+
position: relative;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.transcript-search-box input {
|
|
964
|
+
width: 100%;
|
|
965
|
+
padding: 10px 36px 10px 12px;
|
|
966
|
+
border: 1px solid #e0e0e0;
|
|
967
|
+
border-radius: 6px;
|
|
968
|
+
font-size: 13px;
|
|
969
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto';
|
|
970
|
+
background: #ffffff;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.transcript-search-box input:focus {
|
|
974
|
+
outline: none;
|
|
975
|
+
border-color: #2196F3;
|
|
976
|
+
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.transcript-search-clear {
|
|
980
|
+
position: absolute;
|
|
981
|
+
right: 10px;
|
|
982
|
+
top: 50%;
|
|
983
|
+
transform: translateY(-50%);
|
|
984
|
+
background: none;
|
|
985
|
+
border: none;
|
|
986
|
+
color: #999;
|
|
987
|
+
cursor: pointer;
|
|
988
|
+
display: none;
|
|
989
|
+
font-size: 18px;
|
|
990
|
+
padding: 0;
|
|
991
|
+
width: 20px;
|
|
992
|
+
height: 20px;
|
|
993
|
+
transition: color 0.2s;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.transcript-search-clear:hover {
|
|
997
|
+
color: #666;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.transcript-search-box input:not(:placeholder-shown) ~ .transcript-search-clear {
|
|
1001
|
+
display: block;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.transcript-log {
|
|
1005
|
+
background: #f9f9f9;
|
|
1006
|
+
border: 1px solid #e8eaed;
|
|
1007
|
+
border-radius: 6px;
|
|
1008
|
+
padding: 16px;
|
|
1009
|
+
max-height: 600px;
|
|
1010
|
+
overflow-y: auto;
|
|
1011
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.transcript-entry {
|
|
1015
|
+
display: flex;
|
|
1016
|
+
gap: 12px;
|
|
1017
|
+
margin: 6px 0;
|
|
1018
|
+
padding: 4px 0;
|
|
1019
|
+
border-bottom: 1px solid #f0f0f0;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.transcript-entry:last-child {
|
|
1023
|
+
border-bottom: none;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.transcript-entry.hidden {
|
|
1027
|
+
display: none;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
.transcript-time {
|
|
1031
|
+
color: #999;
|
|
1032
|
+
min-width: 100px;
|
|
1033
|
+
flex-shrink: 0;
|
|
1034
|
+
font-size: 11px;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.transcript-type {
|
|
1038
|
+
min-width: 80px;
|
|
1039
|
+
flex-shrink: 0;
|
|
1040
|
+
font-size: 11px;
|
|
1041
|
+
font-weight: 600;
|
|
1042
|
+
text-transform: uppercase;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.transcript-type.connection { color: #9c27b0; }
|
|
1046
|
+
.transcript-type.command { color: #2196F3; }
|
|
1047
|
+
.transcript-type.response { color: #34a853; }
|
|
1048
|
+
.transcript-type.tls { color: #ff9800; }
|
|
1049
|
+
.transcript-type.data { color: #607d8b; }
|
|
1050
|
+
.transcript-type.error { color: #f44336; }
|
|
1051
|
+
|
|
1052
|
+
.transcript-direction {
|
|
1053
|
+
min-width: 60px;
|
|
1054
|
+
flex-shrink: 0;
|
|
1055
|
+
font-size: 11px;
|
|
1056
|
+
color: #666;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.transcript-direction.client::before {
|
|
1060
|
+
content: '→ ';
|
|
1061
|
+
color: #2196F3;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
.transcript-direction.server::before {
|
|
1065
|
+
content: '← ';
|
|
1066
|
+
color: #34a853;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.transcript-message {
|
|
1070
|
+
flex: 1;
|
|
1071
|
+
color: #1a1a1a;
|
|
1072
|
+
font-size: 12px;
|
|
1073
|
+
word-break: break-word;
|
|
1074
|
+
white-space: pre-wrap;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.transcript-message.error {
|
|
1078
|
+
color: #f44336;
|
|
1079
|
+
font-weight: 500;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.transcript-empty {
|
|
1083
|
+
text-align: center;
|
|
1084
|
+
padding: 40px 20px;
|
|
1085
|
+
color: #999;
|
|
1086
|
+
font-size: 14px;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
897
1089
|
/* Encryption/Signature Tooltip Styling */
|
|
898
1090
|
.encryption-tooltip-content {
|
|
899
1091
|
padding: 12px 16px;
|
|
@@ -1224,6 +1416,7 @@
|
|
|
1224
1416
|
<ul class="views-tabs">
|
|
1225
1417
|
<li class="format tab html selected" data-message-format="html"><a href="#">HTML</a></li>
|
|
1226
1418
|
<li class="format tab plain" data-message-format="plain"><a href="#">Plain Text</a></li>
|
|
1419
|
+
<li class="format tab transcript" data-message-format="transcript"><a href="#">Transcript</a></li>
|
|
1227
1420
|
<li class="format tab source" data-message-format="source">
|
|
1228
1421
|
<a href="#">Source</a>
|
|
1229
1422
|
<button class="copy-source-btn" id="copySourceBtn" title="Copy email source to clipboard">
|
|
@@ -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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephane Paquet
|
|
@@ -325,24 +325,11 @@ dependencies:
|
|
|
325
325
|
- - ">="
|
|
326
326
|
- !ruby/object:Gem::Version
|
|
327
327
|
version: 4.2.1
|
|
328
|
-
description:
|
|
329
|
-
|
|
328
|
+
description: 'MailCatcher NG brings the beloved MailCatcher into the modern era: fast,
|
|
329
|
+
reliable SMTP catching with a stunning real-time interface, updated dependencies,
|
|
330
|
+
and powerful email inspection features
|
|
330
331
|
|
|
331
|
-
|
|
332
|
-
and provides a beautiful web interface (default: http://127.0.0.1:1080) to view messages in real-time.
|
|
333
|
-
|
|
334
|
-
Features include:
|
|
335
|
-
• Instant updates via WebSockets (with polling fallback)
|
|
336
|
-
• Elegant UI with HTML, plain text, raw source, and attachment views
|
|
337
|
-
• Download original .eml files
|
|
338
|
-
• Keyboard navigation and mobile-friendly design
|
|
339
|
-
• Updated dependencies compatible with Ruby 3.2+
|
|
340
|
-
• Email authentication verification (DMARC, DKIM, SPF), encryption support, BIMI preview, and more
|
|
341
|
-
|
|
342
|
-
Run `mailcatcher`, configure your app to send via `smtp://127.0.0.1:1025`,
|
|
343
|
-
then visit http://127.0.0.1:1080 to inspect captured emails – perfect for development & testing.
|
|
344
|
-
|
|
345
|
-
Note: This is the `mailcatcher-ng` gem – the executable remains `mailcatcher` for maximum compatibility.
|
|
332
|
+
'
|
|
346
333
|
email: contact@thepew.io
|
|
347
334
|
executables:
|
|
348
335
|
- catchmail
|
|
@@ -371,6 +358,7 @@ files:
|
|
|
371
358
|
- views/404.erb
|
|
372
359
|
- views/index.erb
|
|
373
360
|
- views/server_info.erb
|
|
361
|
+
- views/transcript.erb
|
|
374
362
|
- views/websocket_test.erb
|
|
375
363
|
homepage: https://spaquet.github.io/mailcatcher/
|
|
376
364
|
licenses:
|