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 +4 -4
- data/lib/mail_catcher/version.rb +1 -1
- data/lib/mail_catcher/web/application.rb +129 -18
- 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: 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,7 +498,7 @@ 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({
|
|
@@ -425,7 +523,7 @@ module MailCatcher
|
|
|
425
523
|
end
|
|
426
524
|
|
|
427
525
|
get "/messages/:id.html" do
|
|
428
|
-
id = params[:id]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
880
|
+
id = valid_message_id!(params[:id])
|
|
770
881
|
if Mail.message(id)
|
|
771
882
|
Mail.delete_message!(id)
|
|
772
883
|
status 204
|