mailcatcher-ng 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/mail_catcher/mail.rb +83 -0
- data/lib/mail_catcher/smtp.rb +220 -10
- data/lib/mail_catcher/version.rb +1 -1
- data/lib/mail_catcher/web/application.rb +23 -2
- data/public/assets/mailcatcher-ui.js +536 -0
- data/public/assets/mailcatcher.css +1303 -0
- data/public/assets/mailcatcher.js +1 -1
- data/views/index.erb +5 -1622
- data/views/transcript.erb +240 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71c31256fbd4bdc76749770fe321db1351839f23fd01d1ef2ab6f21382da774b
|
|
4
|
+
data.tar.gz: 7f77c22a8dd2e3b7cea54c6ec415af7f408ad39588e48772cffaec625dd8fd15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e47f2938af045fd8da6b1e7eacb1a0552a5076bd5415c1079db271335271fc1299c3c47e3dfc78b9eaf1383699f339c1b7f674ff3169bdf65f270666c819255
|
|
7
|
+
data.tar.gz: c49f21a836aae6a35f84568997ddc29f4c619b3cebead68421a882b4ab3ff5ae4685f39e7a69a0473840d83831f88b55b01b4cbe0af5399981f04663319e65f5
|
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])
|