mailcatcher-ng 1.5.3 → 1.5.7
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/version.rb +1 -1
- data/lib/mail_catcher/web/application.rb +130 -18
- data/public/assets/jquery.min.js +2 -2
- data/public/assets/jquery.min.map +1 -1
- data/public/assets/mailcatcher.css +20 -0
- data/public/assets/mailcatcher.js +57 -11
- data/views/index.erb +1 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ebd7047c9f071ee748d83a3d3b1cbe961c932a00b87b590293905d5dd31c183
|
|
4
|
+
data.tar.gz: 6003d4d52ec43a584d9be2e42e27c99db46344fd36f574e0425afebfdc721a7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca3f6e06e9ca31c17f82b5b342ba27c9d66efca42feac6f8ab80623200ec111ce839cb5954b92851577f31f1386ed3e1ba6cd084345fb14ec8b6e002cb3b147f
|
|
7
|
+
data.tar.gz: d3693df74e695a5826121b9a6ad3c78820bf34717baae6b62f449a723c0290263f3672cc73d1de70fc084b9552d9b68e3ef7648a1976005f6660aa0fe461388e
|
data/lib/mail_catcher/version.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
4
|
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "ipaddr"
|
|
5
7
|
require "uri"
|
|
6
8
|
|
|
7
9
|
require "faye/websocket"
|
|
@@ -34,6 +36,13 @@ end
|
|
|
34
36
|
module MailCatcher
|
|
35
37
|
module Web
|
|
36
38
|
class Application < Sinatra::Base
|
|
39
|
+
class RemoteResourceTooLarge < StandardError; end
|
|
40
|
+
|
|
41
|
+
REMOTE_RESOURCE_MAX_BYTES = 5 * 1024 * 1024
|
|
42
|
+
REMOTE_RESOURCE_OPEN_TIMEOUT = 3
|
|
43
|
+
REMOTE_RESOURCE_READ_TIMEOUT = 5
|
|
44
|
+
REMOTE_RESOURCE_MAX_REDIRECTS = 3
|
|
45
|
+
|
|
37
46
|
set :environment, MailCatcher.env
|
|
38
47
|
set :prefix, MailCatcher.options[:http_path]
|
|
39
48
|
set :asset_prefix, File.join(prefix, "assets")
|
|
@@ -226,6 +235,95 @@ module MailCatcher
|
|
|
226
235
|
end
|
|
227
236
|
end
|
|
228
237
|
|
|
238
|
+
helpers do
|
|
239
|
+
def valid_message_id!(id)
|
|
240
|
+
message_id = Integer(id)
|
|
241
|
+
halt 400, "Invalid message id" if message_id.negative?
|
|
242
|
+
message_id
|
|
243
|
+
rescue ArgumentError, TypeError
|
|
244
|
+
halt 400, "Invalid message id"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def remote_resource_proxy_url(url)
|
|
248
|
+
"/resources/proxy?url=#{URI.encode_www_form_component(url)}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def external_http_url?(raw_url)
|
|
252
|
+
uri = URI.parse(raw_url)
|
|
253
|
+
uri.is_a?(URI::HTTP) && uri.host && uri.host != ""
|
|
254
|
+
rescue URI::InvalidURIError
|
|
255
|
+
false
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def safe_remote_host?(host)
|
|
259
|
+
return false if host.nil? || host.empty?
|
|
260
|
+
return false if host == "localhost"
|
|
261
|
+
|
|
262
|
+
ip = IPAddr.new(host)
|
|
263
|
+
!ip.loopback? && !ip.private? && !ip.link_local?
|
|
264
|
+
rescue IPAddr::InvalidAddressError
|
|
265
|
+
require "resolv"
|
|
266
|
+
addresses = Resolv.getaddresses(host)
|
|
267
|
+
return false if addresses.empty?
|
|
268
|
+
|
|
269
|
+
addresses.all? do |address|
|
|
270
|
+
ip = IPAddr.new(address)
|
|
271
|
+
!ip.loopback? && !ip.private? && !ip.link_local?
|
|
272
|
+
rescue IPAddr::InvalidAddressError
|
|
273
|
+
false
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def rewrite_html_for_preview(html)
|
|
278
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(html.to_s)
|
|
279
|
+
|
|
280
|
+
doc.css("img[src], source[src], iframe[src], video[src], audio[src], object[data], embed[src]").each do |node|
|
|
281
|
+
attr_name = node.name == "object" ? "data" : (node["src"] ? "src" : "href")
|
|
282
|
+
raw_url = node[attr_name]
|
|
283
|
+
next unless raw_url
|
|
284
|
+
next unless external_http_url?(raw_url)
|
|
285
|
+
|
|
286
|
+
node[attr_name] = remote_resource_proxy_url(raw_url)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
doc.to_html
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def fetch_remote_resource(url, depth = 0)
|
|
293
|
+
raise Sinatra::BadRequest, "Invalid URL" unless external_http_url?(url)
|
|
294
|
+
raise Sinatra::BadRequest, "Too many redirects" if depth > REMOTE_RESOURCE_MAX_REDIRECTS
|
|
295
|
+
|
|
296
|
+
uri = URI.parse(url)
|
|
297
|
+
raise Sinatra::BadRequest, "Unsafe remote host" unless safe_remote_host?(uri.host)
|
|
298
|
+
|
|
299
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
300
|
+
http.use_ssl = uri.scheme == "https"
|
|
301
|
+
http.open_timeout = REMOTE_RESOURCE_OPEN_TIMEOUT
|
|
302
|
+
http.read_timeout = REMOTE_RESOURCE_READ_TIMEOUT
|
|
303
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl?
|
|
304
|
+
|
|
305
|
+
response = http.request(Net::HTTP::Get.new(uri.request_uri))
|
|
306
|
+
|
|
307
|
+
case response
|
|
308
|
+
when Net::HTTPSuccess
|
|
309
|
+
body = +""
|
|
310
|
+
response.read_body do |chunk|
|
|
311
|
+
body << chunk
|
|
312
|
+
raise RemoteResourceTooLarge if body.bytesize > REMOTE_RESOURCE_MAX_BYTES
|
|
313
|
+
end
|
|
314
|
+
[response["content-type"], body]
|
|
315
|
+
when Net::HTTPRedirection
|
|
316
|
+
location = response["location"]
|
|
317
|
+
raise Sinatra::BadRequest, "Redirect missing location" unless location
|
|
318
|
+
|
|
319
|
+
next_url = URI.parse(location).absolute? ? location : uri.merge(location).to_s
|
|
320
|
+
fetch_remote_resource(next_url, depth + 1)
|
|
321
|
+
else
|
|
322
|
+
halt response.code.to_i, response.message
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
229
327
|
get "/" do
|
|
230
328
|
@version = MailCatcher::VERSION
|
|
231
329
|
erb :index
|
|
@@ -400,10 +498,11 @@ module MailCatcher
|
|
|
400
498
|
end
|
|
401
499
|
|
|
402
500
|
get "/messages/:id.json" do
|
|
403
|
-
id = params[:id]
|
|
501
|
+
id = valid_message_id!(params[:id])
|
|
404
502
|
if message = Mail.message(id)
|
|
405
503
|
content_type :json
|
|
406
504
|
JSON.generate(message.merge({
|
|
505
|
+
"content_type" => message["type"],
|
|
407
506
|
"formats" => [
|
|
408
507
|
"source",
|
|
409
508
|
("html" if Mail.message_has_html? id),
|
|
@@ -424,7 +523,7 @@ module MailCatcher
|
|
|
424
523
|
end
|
|
425
524
|
|
|
426
525
|
get "/messages/:id.html" do
|
|
427
|
-
id = params[:id]
|
|
526
|
+
id = valid_message_id!(params[:id])
|
|
428
527
|
if part = Mail.message_part_html(id)
|
|
429
528
|
content_type :html, :charset => (part["charset"] || "utf8")
|
|
430
529
|
|
|
@@ -432,6 +531,7 @@ module MailCatcher
|
|
|
432
531
|
|
|
433
532
|
# Rewrite body to link to embedded attachments served by cid
|
|
434
533
|
body = body.gsub /cid:([^'"> ]+)/, "#{id}/parts/\\1"
|
|
534
|
+
body = rewrite_html_for_preview(body)
|
|
435
535
|
|
|
436
536
|
body
|
|
437
537
|
else
|
|
@@ -440,7 +540,7 @@ module MailCatcher
|
|
|
440
540
|
end
|
|
441
541
|
|
|
442
542
|
get "/messages/:id.plain" do
|
|
443
|
-
id = params[:id]
|
|
543
|
+
id = valid_message_id!(params[:id])
|
|
444
544
|
if part = Mail.message_part_plain(id)
|
|
445
545
|
content_type part["type"], :charset => (part["charset"] || "utf8")
|
|
446
546
|
part["body"]
|
|
@@ -450,7 +550,7 @@ module MailCatcher
|
|
|
450
550
|
end
|
|
451
551
|
|
|
452
552
|
get "/messages/:id.source" do
|
|
453
|
-
id = params[:id]
|
|
553
|
+
id = valid_message_id!(params[:id])
|
|
454
554
|
if message_source = Mail.message_source(id)
|
|
455
555
|
content_type "text/plain"
|
|
456
556
|
message_source
|
|
@@ -460,7 +560,7 @@ module MailCatcher
|
|
|
460
560
|
end
|
|
461
561
|
|
|
462
562
|
get "/messages/:id.eml" do
|
|
463
|
-
id = params[:id]
|
|
563
|
+
id = valid_message_id!(params[:id])
|
|
464
564
|
if message_source = Mail.message_source(id)
|
|
465
565
|
content_type "message/rfc822"
|
|
466
566
|
message_source
|
|
@@ -470,7 +570,7 @@ module MailCatcher
|
|
|
470
570
|
end
|
|
471
571
|
|
|
472
572
|
get "/messages/:id/transcript.json" do
|
|
473
|
-
id = params[:id]
|
|
573
|
+
id = valid_message_id!(params[:id])
|
|
474
574
|
if transcript = Mail.message_transcript(id)
|
|
475
575
|
content_type :json
|
|
476
576
|
JSON.generate(transcript)
|
|
@@ -480,7 +580,7 @@ module MailCatcher
|
|
|
480
580
|
end
|
|
481
581
|
|
|
482
582
|
get "/messages/:id.transcript" do
|
|
483
|
-
id = params[:id]
|
|
583
|
+
id = valid_message_id!(params[:id])
|
|
484
584
|
if transcript = Mail.message_transcript(id)
|
|
485
585
|
content_type :html, charset: "utf-8"
|
|
486
586
|
erb :transcript, locals: { transcript: transcript }
|
|
@@ -490,7 +590,7 @@ module MailCatcher
|
|
|
490
590
|
end
|
|
491
591
|
|
|
492
592
|
get "/messages/:id/parts/:cid" do
|
|
493
|
-
id = params[:id]
|
|
593
|
+
id = valid_message_id!(params[:id])
|
|
494
594
|
if part = Mail.message_part_cid(id, params[:cid])
|
|
495
595
|
content_type part["type"], :charset => (part["charset"] || "utf8")
|
|
496
596
|
attachment part["filename"] if part["is_attachment"] == 1
|
|
@@ -501,7 +601,7 @@ module MailCatcher
|
|
|
501
601
|
end
|
|
502
602
|
|
|
503
603
|
get "/messages/:id/extract" do
|
|
504
|
-
id = params[:id]
|
|
604
|
+
id = valid_message_id!(params[:id])
|
|
505
605
|
if message = Mail.message(id)
|
|
506
606
|
content_type :json
|
|
507
607
|
JSON.generate(Mail.extract_tokens(id, type: params[:type]))
|
|
@@ -511,7 +611,7 @@ module MailCatcher
|
|
|
511
611
|
end
|
|
512
612
|
|
|
513
613
|
get "/messages/:id/links.json" do
|
|
514
|
-
id = params[:id]
|
|
614
|
+
id = valid_message_id!(params[:id])
|
|
515
615
|
if message = Mail.message(id)
|
|
516
616
|
content_type :json
|
|
517
617
|
JSON.generate(Mail.extract_all_links(id))
|
|
@@ -521,7 +621,7 @@ module MailCatcher
|
|
|
521
621
|
end
|
|
522
622
|
|
|
523
623
|
get "/messages/:id/parsed.json" do
|
|
524
|
-
id = params[:id]
|
|
624
|
+
id = valid_message_id!(params[:id])
|
|
525
625
|
if message = Mail.message(id)
|
|
526
626
|
content_type :json
|
|
527
627
|
JSON.generate(Mail.parse_message_structured(id))
|
|
@@ -531,7 +631,7 @@ module MailCatcher
|
|
|
531
631
|
end
|
|
532
632
|
|
|
533
633
|
get "/messages/:id/accessibility.json" do
|
|
534
|
-
id = params[:id]
|
|
634
|
+
id = valid_message_id!(params[:id])
|
|
535
635
|
if message = Mail.message(id)
|
|
536
636
|
content_type :json
|
|
537
637
|
begin
|
|
@@ -546,7 +646,7 @@ module MailCatcher
|
|
|
546
646
|
end
|
|
547
647
|
|
|
548
648
|
post "/messages/:id/forward" do
|
|
549
|
-
id = params[:id]
|
|
649
|
+
id = valid_message_id!(params[:id])
|
|
550
650
|
if message = Mail.message(id)
|
|
551
651
|
content_type :json
|
|
552
652
|
result = Mail.forward_message(id)
|
|
@@ -563,7 +663,7 @@ module MailCatcher
|
|
|
563
663
|
end
|
|
564
664
|
|
|
565
665
|
delete "/messages/:id" do
|
|
566
|
-
id = params[:id]
|
|
666
|
+
id = valid_message_id!(params[:id])
|
|
567
667
|
if Mail.message(id)
|
|
568
668
|
Mail.delete_message!(id)
|
|
569
669
|
status 204
|
|
@@ -572,6 +672,18 @@ module MailCatcher
|
|
|
572
672
|
end
|
|
573
673
|
end
|
|
574
674
|
|
|
675
|
+
get "/resources/proxy" do
|
|
676
|
+
content_type, body = fetch_remote_resource(params[:url].to_s)
|
|
677
|
+
content_type content_type if content_type
|
|
678
|
+
body
|
|
679
|
+
rescue URI::InvalidURIError, Sinatra::BadRequest => e
|
|
680
|
+
halt 400, e.message
|
|
681
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
682
|
+
halt 504, "Remote resource timed out"
|
|
683
|
+
rescue RemoteResourceTooLarge
|
|
684
|
+
halt 413, "Remote resource too large"
|
|
685
|
+
end
|
|
686
|
+
|
|
575
687
|
# Claude Plugin Routes
|
|
576
688
|
# These routes provide Claude Plugin marketplace compatible endpoints
|
|
577
689
|
|
|
@@ -671,7 +783,7 @@ module MailCatcher
|
|
|
671
783
|
end
|
|
672
784
|
|
|
673
785
|
get "/plugin/message/:id/tokens" do
|
|
674
|
-
id = params[:id]
|
|
786
|
+
id = valid_message_id!(params[:id])
|
|
675
787
|
content_type :json
|
|
676
788
|
|
|
677
789
|
unless Mail.message(id)
|
|
@@ -709,7 +821,7 @@ module MailCatcher
|
|
|
709
821
|
end
|
|
710
822
|
|
|
711
823
|
get "/plugin/message/:id/auth-info" do
|
|
712
|
-
id = params[:id]
|
|
824
|
+
id = valid_message_id!(params[:id])
|
|
713
825
|
content_type :json
|
|
714
826
|
|
|
715
827
|
unless Mail.message(id)
|
|
@@ -732,7 +844,7 @@ module MailCatcher
|
|
|
732
844
|
end
|
|
733
845
|
|
|
734
846
|
get "/plugin/message/:id/preview" do
|
|
735
|
-
id = params[:id]
|
|
847
|
+
id = valid_message_id!(params[:id])
|
|
736
848
|
content_type :html
|
|
737
849
|
|
|
738
850
|
html_part = Mail.message_part_html(id)
|
|
@@ -765,7 +877,7 @@ module MailCatcher
|
|
|
765
877
|
end
|
|
766
878
|
|
|
767
879
|
delete "/plugin/message/:id" do
|
|
768
|
-
id = params[:id]
|
|
880
|
+
id = valid_message_id!(params[:id])
|
|
769
881
|
if Mail.message(id)
|
|
770
882
|
Mail.delete_message!(id)
|
|
771
883
|
status 204
|