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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c294ba20de4249979354e5422b1b9dc2b35ff3d604c8668766e8a07215b7cb2
4
- data.tar.gz: 11e951be074e328589043f096bedd34af47708fe339713c80a505944e88ff890
3
+ metadata.gz: 7ebd7047c9f071ee748d83a3d3b1cbe961c932a00b87b590293905d5dd31c183
4
+ data.tar.gz: 6003d4d52ec43a584d9be2e42e27c99db46344fd36f574e0425afebfdc721a7b
5
5
  SHA512:
6
- metadata.gz: fc2d9d32c9942bc5602406d8d450cb83c1be77e74a8a7bdeb526391ea5f74d7280ff413892082a85c28b459a9f4094d27546d4bf9e4081deb84cae3c1c7b4167
7
- data.tar.gz: 65a4c3d0bd3a561555ac16fc55beebddf56813b4ed2093402e3c7cb5825a7572e1e29e52c0ab97259d3a72ed8150ba5103d438edd236df3d7d8e158ad9e28280
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.3'
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,10 +498,11 @@ 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({
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
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].to_i
880
+ id = valid_message_id!(params[:id])
769
881
  if Mail.message(id)
770
882
  Mail.delete_message!(id)
771
883
  status 204