mailcatcher-ng 1.5.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ee8fa5604d4a8a9fa508ba5f4c0ae42f0fbef6518eca7d965f0e22589a6dc11
4
- data.tar.gz: 5458a59890d42c974e198fa6b0c446082c105f1c300d7fc4cdb4f763ee305134
3
+ metadata.gz: 7ebd7047c9f071ee748d83a3d3b1cbe961c932a00b87b590293905d5dd31c183
4
+ data.tar.gz: 6003d4d52ec43a584d9be2e42e27c99db46344fd36f574e0425afebfdc721a7b
5
5
  SHA512:
6
- metadata.gz: 1ae6e0bed271235f98a24551cd21461034a2d347f8235cb5abeb4d0540e7b1c622ee314d9653711f0fde1152a24eb4a110dd5d1f054eafda474c91f861dda660
7
- data.tar.gz: 205715ec3a806a72a8538cbd3d770a2a9c6d793923b423c711de8010583fd7c15bc8da3da4b31bf1a71edf600bde4dd3a68bafd76c198294457117fb8fd25939
6
+ metadata.gz: ca3f6e06e9ca31c17f82b5b342ba27c9d66efca42feac6f8ab80623200ec111ce839cb5954b92851577f31f1386ed3e1ba6cd084345fb14ec8b6e002cb3b147f
7
+ data.tar.gz: d3693df74e695a5826121b9a6ad3c78820bf34717baae6b62f449a723c0290263f3672cc73d1de70fc084b9552d9b68e3ef7648a1976005f6660aa0fe461388e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MailCatcher
4
- VERSION = '1.5.6'
4
+ VERSION = '1.5.7'
5
5
  end
@@ -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,7 +498,7 @@ module MailCatcher
400
498
  end
401
499
 
402
500
  get "/messages/:id.json" do
403
- id = params[:id].to_i
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({
@@ -425,7 +523,7 @@ module MailCatcher
425
523
  end
426
524
 
427
525
  get "/messages/:id.html" do
428
- id = params[:id].to_i
526
+ id = valid_message_id!(params[:id])
429
527
  if part = Mail.message_part_html(id)
430
528
  content_type :html, :charset => (part["charset"] || "utf8")
431
529
 
@@ -433,6 +531,7 @@ module MailCatcher
433
531
 
434
532
  # Rewrite body to link to embedded attachments served by cid
435
533
  body = body.gsub /cid:([^'"> ]+)/, "#{id}/parts/\\1"
534
+ body = rewrite_html_for_preview(body)
436
535
 
437
536
  body
438
537
  else
@@ -441,7 +540,7 @@ module MailCatcher
441
540
  end
442
541
 
443
542
  get "/messages/:id.plain" do
444
- id = params[:id].to_i
543
+ id = valid_message_id!(params[:id])
445
544
  if part = Mail.message_part_plain(id)
446
545
  content_type part["type"], :charset => (part["charset"] || "utf8")
447
546
  part["body"]
@@ -451,7 +550,7 @@ module MailCatcher
451
550
  end
452
551
 
453
552
  get "/messages/:id.source" do
454
- id = params[:id].to_i
553
+ id = valid_message_id!(params[:id])
455
554
  if message_source = Mail.message_source(id)
456
555
  content_type "text/plain"
457
556
  message_source
@@ -461,7 +560,7 @@ module MailCatcher
461
560
  end
462
561
 
463
562
  get "/messages/:id.eml" do
464
- id = params[:id].to_i
563
+ id = valid_message_id!(params[:id])
465
564
  if message_source = Mail.message_source(id)
466
565
  content_type "message/rfc822"
467
566
  message_source
@@ -471,7 +570,7 @@ module MailCatcher
471
570
  end
472
571
 
473
572
  get "/messages/:id/transcript.json" do
474
- id = params[:id].to_i
573
+ id = valid_message_id!(params[:id])
475
574
  if transcript = Mail.message_transcript(id)
476
575
  content_type :json
477
576
  JSON.generate(transcript)
@@ -481,7 +580,7 @@ module MailCatcher
481
580
  end
482
581
 
483
582
  get "/messages/:id.transcript" do
484
- id = params[:id].to_i
583
+ id = valid_message_id!(params[:id])
485
584
  if transcript = Mail.message_transcript(id)
486
585
  content_type :html, charset: "utf-8"
487
586
  erb :transcript, locals: { transcript: transcript }
@@ -491,7 +590,7 @@ module MailCatcher
491
590
  end
492
591
 
493
592
  get "/messages/:id/parts/:cid" do
494
- id = params[:id].to_i
593
+ id = valid_message_id!(params[:id])
495
594
  if part = Mail.message_part_cid(id, params[:cid])
496
595
  content_type part["type"], :charset => (part["charset"] || "utf8")
497
596
  attachment part["filename"] if part["is_attachment"] == 1
@@ -502,7 +601,7 @@ module MailCatcher
502
601
  end
503
602
 
504
603
  get "/messages/:id/extract" do
505
- id = params[:id].to_i
604
+ id = valid_message_id!(params[:id])
506
605
  if message = Mail.message(id)
507
606
  content_type :json
508
607
  JSON.generate(Mail.extract_tokens(id, type: params[:type]))
@@ -512,7 +611,7 @@ module MailCatcher
512
611
  end
513
612
 
514
613
  get "/messages/:id/links.json" do
515
- id = params[:id].to_i
614
+ id = valid_message_id!(params[:id])
516
615
  if message = Mail.message(id)
517
616
  content_type :json
518
617
  JSON.generate(Mail.extract_all_links(id))
@@ -522,7 +621,7 @@ module MailCatcher
522
621
  end
523
622
 
524
623
  get "/messages/:id/parsed.json" do
525
- id = params[:id].to_i
624
+ id = valid_message_id!(params[:id])
526
625
  if message = Mail.message(id)
527
626
  content_type :json
528
627
  JSON.generate(Mail.parse_message_structured(id))
@@ -532,7 +631,7 @@ module MailCatcher
532
631
  end
533
632
 
534
633
  get "/messages/:id/accessibility.json" do
535
- id = params[:id].to_i
634
+ id = valid_message_id!(params[:id])
536
635
  if message = Mail.message(id)
537
636
  content_type :json
538
637
  begin
@@ -547,7 +646,7 @@ module MailCatcher
547
646
  end
548
647
 
549
648
  post "/messages/:id/forward" do
550
- id = params[:id].to_i
649
+ id = valid_message_id!(params[:id])
551
650
  if message = Mail.message(id)
552
651
  content_type :json
553
652
  result = Mail.forward_message(id)
@@ -564,7 +663,7 @@ module MailCatcher
564
663
  end
565
664
 
566
665
  delete "/messages/:id" do
567
- id = params[:id].to_i
666
+ id = valid_message_id!(params[:id])
568
667
  if Mail.message(id)
569
668
  Mail.delete_message!(id)
570
669
  status 204
@@ -573,6 +672,18 @@ module MailCatcher
573
672
  end
574
673
  end
575
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
+
576
687
  # Claude Plugin Routes
577
688
  # These routes provide Claude Plugin marketplace compatible endpoints
578
689
 
@@ -672,7 +783,7 @@ module MailCatcher
672
783
  end
673
784
 
674
785
  get "/plugin/message/:id/tokens" do
675
- id = params[:id].to_i
786
+ id = valid_message_id!(params[:id])
676
787
  content_type :json
677
788
 
678
789
  unless Mail.message(id)
@@ -710,7 +821,7 @@ module MailCatcher
710
821
  end
711
822
 
712
823
  get "/plugin/message/:id/auth-info" do
713
- id = params[:id].to_i
824
+ id = valid_message_id!(params[:id])
714
825
  content_type :json
715
826
 
716
827
  unless Mail.message(id)
@@ -733,7 +844,7 @@ module MailCatcher
733
844
  end
734
845
 
735
846
  get "/plugin/message/:id/preview" do
736
- id = params[:id].to_i
847
+ id = valid_message_id!(params[:id])
737
848
  content_type :html
738
849
 
739
850
  html_part = Mail.message_part_html(id)
@@ -766,7 +877,7 @@ module MailCatcher
766
877
  end
767
878
 
768
879
  delete "/plugin/message/:id" do
769
- id = params[:id].to_i
880
+ id = valid_message_id!(params[:id])
770
881
  if Mail.message(id)
771
882
  Mail.delete_message!(id)
772
883
  status 204
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.5.6
4
+ version: 1.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephane Paquet