mailcatcher-ng 1.3.1 → 1.4.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 +42 -7
- data/lib/mail_catcher/smtp.rb +8 -6
- data/lib/mail_catcher/version.rb +1 -1
- data/lib/mail_catcher/web/application.rb +30 -0
- data/lib/mail_catcher.rb +5 -0
- data/public/assets/mailcatcher.css +31 -0
- data/public/assets/mailcatcher.js +4 -4
- data/views/index.erb +36 -4
- data/views/server_info.erb +582 -94
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6539793c4e31686b97ba0502cc4c48c47c49ace16230fea4cf2f2cb8d06704c7
|
|
4
|
+
data.tar.gz: d74ed846c8205f2b37b9b433b618424929d12515a928d23b4a481fe03a5993d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd19f4d57d9710adc6c60a21aeeefbfb2623e57d6725463f5bd01b3ce5e5fcb93360d1543301bc9655b64fc814994d036e3c8cf8de3940643f69ccf1f4063297
|
|
7
|
+
data.tar.gz: 7aed22f4e9a54ca7e239f7401d95931cfce90f309d2beae5d28049aa939b4903bb2d256aac8ff694c3740bf62835d6bf2c5f90d8c9b53fd3f855a2b282e6860e
|
data/lib/mail_catcher/mail.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "eventmachine"
|
|
4
|
+
require "fileutils"
|
|
4
5
|
require "json"
|
|
5
6
|
require "mail"
|
|
6
7
|
require "sqlite3"
|
|
@@ -8,9 +9,10 @@ require "sqlite3"
|
|
|
8
9
|
module MailCatcher::Mail extend self
|
|
9
10
|
def db
|
|
10
11
|
@__db ||= begin
|
|
11
|
-
|
|
12
|
+
db_path = determine_db_path
|
|
13
|
+
SQLite3::Database.new(db_path, :type_translation => true).tap do |db|
|
|
12
14
|
db.execute(<<-SQL)
|
|
13
|
-
CREATE TABLE message (
|
|
15
|
+
CREATE TABLE IF NOT EXISTS message (
|
|
14
16
|
id INTEGER PRIMARY KEY ASC,
|
|
15
17
|
sender TEXT,
|
|
16
18
|
recipients TEXT,
|
|
@@ -22,7 +24,7 @@ module MailCatcher::Mail extend self
|
|
|
22
24
|
)
|
|
23
25
|
SQL
|
|
24
26
|
db.execute(<<-SQL)
|
|
25
|
-
CREATE TABLE message_part (
|
|
27
|
+
CREATE TABLE IF NOT EXISTS message_part (
|
|
26
28
|
id INTEGER PRIMARY KEY ASC,
|
|
27
29
|
message_id INTEGER NOT NULL,
|
|
28
30
|
cid TEXT,
|
|
@@ -37,7 +39,7 @@ module MailCatcher::Mail extend self
|
|
|
37
39
|
)
|
|
38
40
|
SQL
|
|
39
41
|
db.execute(<<-SQL)
|
|
40
|
-
CREATE TABLE smtp_transcript (
|
|
42
|
+
CREATE TABLE IF NOT EXISTS smtp_transcript (
|
|
41
43
|
id INTEGER PRIMARY KEY ASC,
|
|
42
44
|
message_id INTEGER,
|
|
43
45
|
session_id TEXT NOT NULL,
|
|
@@ -55,13 +57,25 @@ module MailCatcher::Mail extend self
|
|
|
55
57
|
FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE
|
|
56
58
|
)
|
|
57
59
|
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)")
|
|
60
|
+
db.execute("CREATE INDEX IF NOT EXISTS idx_smtp_transcript_message_id ON smtp_transcript(message_id)")
|
|
61
|
+
db.execute("CREATE INDEX IF NOT EXISTS idx_smtp_transcript_session_id ON smtp_transcript(session_id)")
|
|
60
62
|
db.execute("PRAGMA foreign_keys = ON")
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
def determine_db_path
|
|
68
|
+
if MailCatcher.options && MailCatcher.options[:persistence]
|
|
69
|
+
# Use a persistent SQLite file in the user's home directory
|
|
70
|
+
db_dir = File.expand_path('~/.mailcatcher')
|
|
71
|
+
FileUtils.mkdir_p(db_dir) unless Dir.exist?(db_dir)
|
|
72
|
+
File.join(db_dir, 'mailcatcher.db')
|
|
73
|
+
else
|
|
74
|
+
# Use in-memory database
|
|
75
|
+
':memory:'
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
65
79
|
def add_message(message)
|
|
66
80
|
@add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))")
|
|
67
81
|
|
|
@@ -311,7 +325,10 @@ module MailCatcher::Mail extend self
|
|
|
311
325
|
|
|
312
326
|
def delete!
|
|
313
327
|
@delete_all_messages_query ||= db.prepare "DELETE FROM message"
|
|
328
|
+
@delete_all_transcripts_query ||= db.prepare "DELETE FROM smtp_transcript"
|
|
329
|
+
|
|
314
330
|
@delete_all_messages_query.execute
|
|
331
|
+
@delete_all_transcripts_query.execute
|
|
315
332
|
|
|
316
333
|
EventMachine.next_tick do
|
|
317
334
|
MailCatcher::Bus.push(type: "clear")
|
|
@@ -438,7 +455,7 @@ module MailCatcher::Mail extend self
|
|
|
438
455
|
|
|
439
456
|
def message_transcript(message_id)
|
|
440
457
|
@message_transcript_query ||= db.prepare(<<-SQL)
|
|
441
|
-
SELECT id, session_id, client_ip, client_port,
|
|
458
|
+
SELECT id, message_id, session_id, client_ip, client_port,
|
|
442
459
|
server_ip, server_port, tls_enabled, tls_protocol,
|
|
443
460
|
tls_cipher, connection_started_at, connection_ended_at,
|
|
444
461
|
entries, created_at
|
|
@@ -470,6 +487,24 @@ module MailCatcher::Mail extend self
|
|
|
470
487
|
end
|
|
471
488
|
end
|
|
472
489
|
|
|
490
|
+
def all_transcript_entries
|
|
491
|
+
@all_transcript_entries_query ||= db.prepare(<<-SQL)
|
|
492
|
+
SELECT id, message_id, session_id, client_ip, client_port,
|
|
493
|
+
server_ip, server_port, tls_enabled, tls_protocol,
|
|
494
|
+
tls_cipher, connection_started_at, connection_ended_at,
|
|
495
|
+
entries, created_at
|
|
496
|
+
FROM smtp_transcript
|
|
497
|
+
ORDER BY created_at DESC
|
|
498
|
+
SQL
|
|
499
|
+
|
|
500
|
+
@all_transcript_entries_query.execute.map do |row|
|
|
501
|
+
result = Hash[@all_transcript_entries_query.columns.zip(row)]
|
|
502
|
+
result['entries'] = JSON.parse(result['entries']) if result['entries']
|
|
503
|
+
result['tls_enabled'] = result['tls_enabled'] == 1
|
|
504
|
+
result
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
473
508
|
private
|
|
474
509
|
|
|
475
510
|
def parse_authentication_results(auth_header)
|
data/lib/mail_catcher/smtp.rb
CHANGED
|
@@ -18,6 +18,7 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
18
18
|
@session_id = SecureRandom.uuid
|
|
19
19
|
@connection_started_at = Time.now
|
|
20
20
|
@data_started = false
|
|
21
|
+
@last_message_id = nil
|
|
21
22
|
super
|
|
22
23
|
end
|
|
23
24
|
|
|
@@ -58,8 +59,9 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
58
59
|
@connection_ended_at = Time.now
|
|
59
60
|
log_transcript('connection', 'server', "Connection closed")
|
|
60
61
|
|
|
61
|
-
# Save transcript
|
|
62
|
-
|
|
62
|
+
# Save transcript with the last message if available, otherwise without message_id
|
|
63
|
+
# This ensures "Connection closed" is included in the transcript
|
|
64
|
+
save_transcript(@last_message_id) if @transcript_entries.any?
|
|
63
65
|
|
|
64
66
|
super
|
|
65
67
|
end
|
|
@@ -205,6 +207,7 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
205
207
|
|
|
206
208
|
begin
|
|
207
209
|
message_id = MailCatcher::Mail.add_message current_message
|
|
210
|
+
@last_message_id = message_id
|
|
208
211
|
rescue => e
|
|
209
212
|
$stderr.puts "Error in add_message: #{e.message}"
|
|
210
213
|
$stderr.puts e.backtrace.join("\n")
|
|
@@ -213,16 +216,15 @@ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
|
|
|
213
216
|
|
|
214
217
|
MailCatcher::Mail.delete_older_messages!
|
|
215
218
|
|
|
216
|
-
#
|
|
217
|
-
|
|
219
|
+
# Don't save transcript here - save it when connection closes (in unbind)
|
|
220
|
+
# This ensures "Connection closed" entry is included
|
|
218
221
|
|
|
219
222
|
true
|
|
220
223
|
rescue => exception
|
|
221
224
|
log_transcript('error', 'server', "Exception: #{exception.class} - #{exception.message}")
|
|
222
225
|
MailCatcher.log_exception("Error receiving message", @current_message, exception)
|
|
223
226
|
|
|
224
|
-
#
|
|
225
|
-
save_transcript(nil)
|
|
227
|
+
# Don't save transcript here - save it when connection closes (in unbind)
|
|
226
228
|
|
|
227
229
|
false
|
|
228
230
|
ensure
|
data/lib/mail_catcher/version.rb
CHANGED
|
@@ -101,9 +101,39 @@ module MailCatcher
|
|
|
101
101
|
@fqdn = fqdn
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# Fetch all SMTP transcripts and flatten entries for display
|
|
105
|
+
transcripts = Mail.all_transcript_entries
|
|
106
|
+
@log_entries = transcripts.flat_map do |transcript|
|
|
107
|
+
transcript['entries'].map do |entry|
|
|
108
|
+
entry.merge({
|
|
109
|
+
'session_id' => transcript['session_id'],
|
|
110
|
+
'client_ip' => transcript['client_ip'],
|
|
111
|
+
'server_port' => transcript['server_port'],
|
|
112
|
+
'tls_enabled' => transcript['tls_enabled']
|
|
113
|
+
})
|
|
114
|
+
end
|
|
115
|
+
end.sort_by { |e| e['timestamp'] }
|
|
116
|
+
|
|
104
117
|
erb :server_info
|
|
105
118
|
end
|
|
106
119
|
|
|
120
|
+
get "/logs.json" do
|
|
121
|
+
content_type :json
|
|
122
|
+
transcripts = Mail.all_transcript_entries
|
|
123
|
+
log_entries = transcripts.flat_map do |transcript|
|
|
124
|
+
transcript['entries'].map do |entry|
|
|
125
|
+
entry.merge({
|
|
126
|
+
'session_id' => transcript['session_id'],
|
|
127
|
+
'client_ip' => transcript['client_ip'],
|
|
128
|
+
'server_port' => transcript['server_port'],
|
|
129
|
+
'tls_enabled' => transcript['tls_enabled']
|
|
130
|
+
})
|
|
131
|
+
end
|
|
132
|
+
end.sort_by { |e| e['timestamp'] }
|
|
133
|
+
|
|
134
|
+
JSON.generate(entries: log_entries)
|
|
135
|
+
end
|
|
136
|
+
|
|
107
137
|
delete "/" do
|
|
108
138
|
if MailCatcher.quittable?
|
|
109
139
|
MailCatcher.quit!
|
data/lib/mail_catcher.rb
CHANGED
|
@@ -85,6 +85,7 @@ module MailCatcher
|
|
|
85
85
|
http_port: '1080',
|
|
86
86
|
http_path: '/',
|
|
87
87
|
messages_limit: nil,
|
|
88
|
+
persistence: false,
|
|
88
89
|
verbose: false,
|
|
89
90
|
daemon: !windows?,
|
|
90
91
|
browse: false,
|
|
@@ -157,6 +158,10 @@ module MailCatcher
|
|
|
157
158
|
options[:messages_limit] = count
|
|
158
159
|
end
|
|
159
160
|
|
|
161
|
+
parser.on('--persistence', 'Store messages in a persistent SQLite database file') do
|
|
162
|
+
options[:persistence] = true
|
|
163
|
+
end
|
|
164
|
+
|
|
160
165
|
parser.on('--http-path PATH', String, 'Add a prefix to all HTTP paths') do |path|
|
|
161
166
|
clean_path = Rack::Utils.clean_path_info("/#{path}")
|
|
162
167
|
|
|
@@ -324,6 +324,37 @@ main {
|
|
|
324
324
|
text-align: center;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
#messages th.sortable {
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
user-select: none;
|
|
330
|
+
display: flex;
|
|
331
|
+
align-items: center;
|
|
332
|
+
gap: 6px;
|
|
333
|
+
transition: background-color 0.2s, color 0.2s;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#messages th.sortable:hover {
|
|
337
|
+
background-color: #f0f0f0;
|
|
338
|
+
color: #3f3f3f;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#messages th.sortable .sort-icon {
|
|
342
|
+
width: 14px;
|
|
343
|
+
height: 14px;
|
|
344
|
+
flex-shrink: 0;
|
|
345
|
+
opacity: 0.4;
|
|
346
|
+
transition: opacity 0.2s;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#messages th.sortable:hover .sort-icon {
|
|
350
|
+
opacity: 0.6;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#messages th.sortable.active .sort-icon {
|
|
354
|
+
opacity: 1;
|
|
355
|
+
color: #2196F3;
|
|
356
|
+
}
|
|
357
|
+
|
|
327
358
|
#messages tbody {
|
|
328
359
|
overflow-y: auto;
|
|
329
360
|
flex: 1;
|