jets 1.7.2 → 1.8.0
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/CHANGELOG.md +6 -0
- data/README.md +1 -1
- data/jets.gemspec +2 -0
- data/lib/jets.rb +1 -0
- data/lib/jets/application.rb +45 -21
- data/lib/jets/aws_services.rb +2 -0
- data/lib/jets/aws_services/s3_bucket.rb +26 -0
- data/lib/jets/booter.rb +82 -2
- data/lib/jets/builders/code_builder.rb +20 -7
- data/lib/jets/builders/handler_generator.rb +20 -6
- data/lib/jets/cfn/builders/base_child_builder.rb +5 -17
- data/lib/jets/cfn/builders/parent_builder.rb +1 -1
- data/lib/jets/commands/build.rb +6 -0
- data/lib/jets/commands/deploy.rb +10 -0
- data/lib/jets/commands/templates/skeleton/config/environments/development.rb +4 -1
- data/lib/jets/commands/templates/skeleton/config/environments/production.rb +6 -1
- data/lib/jets/commands/templates/skeleton/config/environments/test.rb +7 -0
- data/lib/jets/controller/base.rb +1 -2
- data/lib/jets/controller/rendering/rack_renderer.rb +11 -3
- data/lib/jets/core.rb +5 -72
- data/lib/jets/internal/app/controllers/jets/mailers_controller.rb +97 -0
- data/lib/jets/internal/app/helpers/jets/mailers_helper.rb +9 -0
- data/lib/jets/internal/app/shared/functions/jets/s3_bucket_config.rb +43 -0
- data/lib/jets/internal/app/views/jets/mailers/email.html.erb +145 -0
- data/lib/jets/internal/app/views/jets/mailers/index.html.erb +8 -0
- data/lib/jets/internal/app/views/jets/mailers/mailer.html.erb +6 -0
- data/lib/jets/job/base.rb +10 -0
- data/lib/jets/job/dsl.rb +2 -0
- data/lib/jets/job/dsl/s3_event.rb +36 -0
- data/lib/jets/job/dsl/sns_event.rb +8 -0
- data/lib/jets/job/s3_event_helper.rb +13 -0
- data/lib/jets/lambda/dsl.rb +11 -1
- data/lib/jets/mailer.rb +51 -0
- data/lib/jets/resource/child_stack/app_class.rb +6 -15
- data/lib/jets/resource/child_stack/shared.rb +3 -1
- data/lib/jets/resource/events/rule.rb +1 -1
- data/lib/jets/resource/lambda/event_source_mapping.rb +1 -1
- data/lib/jets/resource/permission.rb +1 -1
- data/lib/jets/resource/replacer.rb +8 -0
- data/lib/jets/resource/s3.rb +3 -17
- data/lib/jets/resource/s3/bucket.rb +24 -0
- data/lib/jets/resource/sns.rb +1 -0
- data/lib/jets/resource/sns/subscription.rb +1 -1
- data/lib/jets/resource/sns/topic.rb +1 -1
- data/lib/jets/resource/sns/topic_policy.rb +40 -0
- data/lib/jets/resource/sqs/queue.rb +1 -1
- data/lib/jets/stack.rb +19 -3
- data/lib/jets/stack/builder.rb +6 -1
- data/lib/jets/stack/depends.rb +36 -0
- data/lib/jets/stack/depends/item.rb +9 -0
- data/lib/jets/stack/function.rb +19 -10
- data/lib/jets/stack/main/dsl.rb +4 -0
- data/lib/jets/stack/main/extensions/iam.rb +8 -0
- data/lib/jets/stack/main/extensions/lambda.rb +20 -7
- data/lib/jets/stack/main/extensions/s3.rb +12 -0
- data/lib/jets/stack/main/extensions/sns.rb +4 -0
- data/lib/jets/stack/s3_event.rb +87 -0
- data/lib/jets/turbine.rb +11 -0
- data/lib/jets/version.rb +1 -1
- metadata +48 -2
@@ -23,7 +23,7 @@ class Jets::Cfn::Builders
|
|
23
23
|
|
24
24
|
def build_minimal_resources
|
25
25
|
# Initial s3 bucket, used to store code zipfile and templates Jets generates
|
26
|
-
resource = Jets::Resource::S3.new
|
26
|
+
resource = Jets::Resource::S3::Bucket.new(logical_id: "s3_bucket")
|
27
27
|
add_resource(resource)
|
28
28
|
add_outputs(resource.outputs)
|
29
29
|
|
data/lib/jets/commands/build.rb
CHANGED
@@ -170,6 +170,9 @@ module Jets::Commands
|
|
170
170
|
end
|
171
171
|
|
172
172
|
# Add internal Jets controllers if they are being used
|
173
|
+
# TODO: Interesting, this eventually just used to generate handlers and controllers only.
|
174
|
+
# Maybe rename to make that clear.
|
175
|
+
# The copying of other internal files like views is done in builders/code_builder.rb copy_internal_jets_code
|
173
176
|
def self.internal_app_files
|
174
177
|
paths = []
|
175
178
|
controllers = File.expand_path("../../internal/app/controllers/jets", __FILE__)
|
@@ -180,6 +183,9 @@ module Jets::Commands
|
|
180
183
|
rack_catchall = Jets::Router.has_controller?("Jets::RackController")
|
181
184
|
paths << "#{controllers}/rack_controller.rb" if rack_catchall
|
182
185
|
|
186
|
+
mailer_controller = Jets::Router.has_controller?("Jets::MailersController")
|
187
|
+
paths << "#{controllers}/mailers_controller.rb" if mailer_controller
|
188
|
+
|
183
189
|
if Jets.config.prewarm.enable
|
184
190
|
jobs = File.expand_path("../../internal/app/jobs/jets", __FILE__)
|
185
191
|
paths << "#{jobs}/preheat_job.rb"
|
data/lib/jets/commands/deploy.rb
CHANGED
@@ -26,11 +26,21 @@ module Jets::Commands
|
|
26
26
|
|
27
27
|
# Build code after the minimal stack because need s3 bucket for assets
|
28
28
|
# on_aws? and s3_base_url logic
|
29
|
+
# TODO: possible deploy hook point: before_build
|
29
30
|
build_code
|
30
31
|
|
32
|
+
# TODO: possible deploy hook point: before_ship
|
33
|
+
create_s3_event_buckets
|
31
34
|
ship(stack_type: :full, s3_bucket: s3_bucket)
|
32
35
|
end
|
33
36
|
|
37
|
+
def create_s3_event_buckets
|
38
|
+
buckets = Jets::Job::Base.s3_events.keys
|
39
|
+
buckets.each do |bucket|
|
40
|
+
Jets::AwsServices::S3Bucket.ensure_exists(bucket)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
34
44
|
def delete_minimal_stack
|
35
45
|
puts "Existing stack is in ROLLBACK_COMPLETE state from a previous failed minimal deploy. Deleting stack and continuing."
|
36
46
|
cfn.delete_stack(stack_name: stack_name)
|
@@ -1,4 +1,9 @@
|
|
1
1
|
Jets.application.configure do
|
2
2
|
# Example:
|
3
3
|
# config.function.memory_size = 2048
|
4
|
-
|
4
|
+
|
5
|
+
# Ignore bad email addresses and do not raise email delivery errors.
|
6
|
+
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
7
|
+
# Docs: http://rubyonjets.com/docs/email-sending/
|
8
|
+
# config.action_mailer.raise_delivery_errors = false
|
9
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
Jets.application.configure do
|
2
|
+
# Tell Action Mailer not to deliver emails to the real world.
|
3
|
+
# The :test delivery method accumulates sent emails in the
|
4
|
+
# ActionMailer::Base.deliveries array.
|
5
|
+
# Docs: http://rubyonjets.com/docs/email-sending/
|
6
|
+
config.action_mailer.delivery_method = :test
|
7
|
+
end
|
data/lib/jets/controller/base.rb
CHANGED
@@ -56,8 +56,7 @@ class Jets::Controller
|
|
56
56
|
def log_info_start
|
57
57
|
display_event = @event.dup
|
58
58
|
display_event['body'] = '[BASE64_ENCODED]' if @event['isBase64Encoded']
|
59
|
-
#
|
60
|
-
# CloudWatch but not locally. This is what we want.
|
59
|
+
# JSON.dump makes logging look pretty in CloudWatch logs because it keeps it on 1 line
|
61
60
|
ip = request.ip
|
62
61
|
Jets.logger.info "Started #{@event['httpMethod']} \"#{@event['path']}\" for #{ip} at #{Time.now}"
|
63
62
|
Jets.logger.info "Processing #{self.class.name}##{@meth}"
|
@@ -30,6 +30,7 @@ module Jets::Controller::Rendering
|
|
30
30
|
if drop_content_info?(status)
|
31
31
|
body = StringIO.new
|
32
32
|
else
|
33
|
+
|
33
34
|
renderer = ActionController::Base.renderer.new(renderer_options)
|
34
35
|
body = renderer.render(render_options)
|
35
36
|
body = StringIO.new(body)
|
@@ -191,7 +192,7 @@ module Jets::Controller::Rendering
|
|
191
192
|
require "jets/overrides/rails"
|
192
193
|
|
193
194
|
# Load helpers
|
194
|
-
# Assign local variable because
|
195
|
+
# Assign local variable because scope in the `:action_view do` block changes
|
195
196
|
app_helper_classes = find_app_helper_classes
|
196
197
|
ActiveSupport.on_load :action_view do
|
197
198
|
include ApplicationHelper # include first
|
@@ -207,11 +208,18 @@ module Jets::Controller::Rendering
|
|
207
208
|
|
208
209
|
# Does not include ApplicationHelper, will include ApplicationHelper explicitly first.
|
209
210
|
def find_app_helper_classes
|
211
|
+
internal_path = File.expand_path("../../internal", File.dirname(__FILE__))
|
212
|
+
internal_classes = find_app_helper_classes_from(internal_path)
|
213
|
+
app_classes = find_app_helper_classes_from(Jets.root)
|
214
|
+
(internal_classes + app_classes).uniq
|
215
|
+
end
|
216
|
+
|
217
|
+
def find_app_helper_classes_from(project_root)
|
210
218
|
klasses = []
|
211
|
-
expression = "#{
|
219
|
+
expression = "#{project_root}/app/helpers/**/*"
|
212
220
|
Dir.glob(expression).each do |path|
|
213
221
|
next unless File.file?(path)
|
214
|
-
class_name = path.sub("#{
|
222
|
+
class_name = path.sub("#{project_root}/app/helpers/","").sub(/\.rb/,'')
|
215
223
|
unless class_name == "application_helper"
|
216
224
|
klasses << class_name.classify.constantize # autoload
|
217
225
|
end
|
data/lib/jets/core.rb
CHANGED
@@ -55,72 +55,6 @@ module Jets::Core
|
|
55
55
|
Jets::VERSION
|
56
56
|
end
|
57
57
|
|
58
|
-
def eager_load!
|
59
|
-
eager_load_jets
|
60
|
-
eager_load_app
|
61
|
-
end
|
62
|
-
|
63
|
-
# Eager load jet's lib and classes
|
64
|
-
def eager_load_jets
|
65
|
-
lib_jets = File.expand_path(".", File.dirname(__FILE__))
|
66
|
-
Dir.glob("#{lib_jets}/**/*.rb").select do |path|
|
67
|
-
next if !File.file?(path)
|
68
|
-
next if skip_eager_load_paths?(path)
|
69
|
-
|
70
|
-
path = path.sub("#{lib_jets}/","jets/")
|
71
|
-
class_name = path
|
72
|
-
.sub(/\.rb$/,'') # remove .rb
|
73
|
-
.sub(/^\.\//,'') # remove ./
|
74
|
-
.sub(/app\/\w+\//,'') # remove app/controllers or app/jobs etc
|
75
|
-
.camelize
|
76
|
-
# special class mappings
|
77
|
-
class_name = class_mappings(class_name)
|
78
|
-
class_name.constantize # use constantize instead of require so dont have to worry about order.
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# Skip these paths because eager loading doesnt work for them.
|
83
|
-
def skip_eager_load_paths?(path)
|
84
|
-
path =~ %r{/cli} ||
|
85
|
-
path =~ %r{/core_ext} ||
|
86
|
-
path =~ %r{/default/application} ||
|
87
|
-
path =~ %r{/functions} ||
|
88
|
-
path =~ %r{/internal/app} ||
|
89
|
-
path =~ %r{/jets/stack} ||
|
90
|
-
path =~ %r{/overrides} ||
|
91
|
-
path =~ %r{/rackup_wrappers} ||
|
92
|
-
path =~ %r{/reconfigure_rails} ||
|
93
|
-
path =~ %r{/templates/} ||
|
94
|
-
path =~ %r{/turbo/project/} ||
|
95
|
-
path =~ %r{/version} ||
|
96
|
-
path =~ %r{/webpacker} ||
|
97
|
-
path =~ %r{/jets/spec}
|
98
|
-
end
|
99
|
-
|
100
|
-
def class_mappings(class_name)
|
101
|
-
map = {
|
102
|
-
"Jets::Io" => "Jets::IO",
|
103
|
-
}
|
104
|
-
map[class_name] || class_name
|
105
|
-
end
|
106
|
-
|
107
|
-
# Eager load user's application
|
108
|
-
def eager_load_app
|
109
|
-
Dir.glob("#{Jets.root}/app/**/*.rb").select do |path|
|
110
|
-
next if !File.file?(path) or path =~ %r{/javascript/} or path =~ %r{/views/}
|
111
|
-
next if path.include?('app/functions') || path.include?('app/shared/functions') || path.include?('app/internal/functions')
|
112
|
-
|
113
|
-
class_name = path
|
114
|
-
.sub(/\.rb$/,'') # remove .rb
|
115
|
-
.sub(%{^\./},'') # remove ./
|
116
|
-
.sub("#{Jets.root}/",'')
|
117
|
-
.sub(%r{app/shared/\w+/},'') # remove shared/resources or shared/extensions
|
118
|
-
.sub(%r{app/\w+/},'') # remove app/controllers or app/jobs etc
|
119
|
-
class_name = class_name.classify
|
120
|
-
class_name.constantize # use constantize instead of require so dont have to worry about order.
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
58
|
# NOTE: In development this will always be 1 because the app gets reloaded.
|
125
59
|
# On AWS Lambda, this will be ever increasing until the container gets replaced.
|
126
60
|
@@call_count = 0
|
@@ -161,18 +95,17 @@ module Jets::Core
|
|
161
95
|
end
|
162
96
|
|
163
97
|
def on_exception(exception)
|
164
|
-
Jets::
|
165
|
-
reporters = subclass.on_exceptions || []
|
166
|
-
reporters.each do |label, block|
|
167
|
-
block.call(exception)
|
168
|
-
end
|
169
|
-
end
|
98
|
+
Jets::Booter.run_turbines(:on_exceptions)
|
170
99
|
end
|
171
100
|
|
172
101
|
def custom_domain?
|
173
102
|
Jets.config.domain.hosted_zone_name
|
174
103
|
end
|
175
104
|
|
105
|
+
def s3_event?
|
106
|
+
!Jets::Job::Base.s3_events.empty?
|
107
|
+
end
|
108
|
+
|
176
109
|
def process(event, context, handler)
|
177
110
|
if event['_prewarm']
|
178
111
|
Jets.increase_prewarm_count
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jets::MailersController < Jets::Controller::Base # :nodoc:
|
4
|
+
before_action :find_preview, only: [:preview]
|
5
|
+
before_action :set_locale, only: [:preview]
|
6
|
+
# TODO: allow multiple actions
|
7
|
+
# before_action :find_preview, :set_locale, only: :preview
|
8
|
+
|
9
|
+
# TODO: add helper_method support
|
10
|
+
# helper_method :part_query, :locale_query
|
11
|
+
|
12
|
+
# TODO: content_security_policy
|
13
|
+
# content_security_policy(false)
|
14
|
+
|
15
|
+
def index
|
16
|
+
@previews = ActionMailer::Preview.all
|
17
|
+
@page_title = "Mailer Previews"
|
18
|
+
end
|
19
|
+
|
20
|
+
def preview
|
21
|
+
if params[:path] == @preview.preview_name
|
22
|
+
@page_title = "Mailer Previews for #{@preview.preview_name}"
|
23
|
+
render :mailer
|
24
|
+
else
|
25
|
+
@email_action = File.basename(params[:path])
|
26
|
+
|
27
|
+
if @preview.email_exists?(@email_action)
|
28
|
+
@email = @preview.call(@email_action, params)
|
29
|
+
|
30
|
+
if params[:part]
|
31
|
+
part_type = Mime::Type.lookup(params[:part])
|
32
|
+
|
33
|
+
if part = find_part(part_type)
|
34
|
+
response.headers["Content-Type"] = params[:part]
|
35
|
+
render plain: part.respond_to?(:decoded) ? part.decoded : part
|
36
|
+
else
|
37
|
+
raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}"
|
38
|
+
end
|
39
|
+
else
|
40
|
+
@part = find_preferred_part(Mime[:html], Mime[:text])
|
41
|
+
render :email, layout: false, formats: %w[html]
|
42
|
+
end
|
43
|
+
else
|
44
|
+
raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def show_previews? # :doc:
|
51
|
+
ActionMailer::Base.show_previews
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_preview # :doc:
|
55
|
+
candidates = []
|
56
|
+
params[:path].to_s.scan(%r{/|$}) { candidates << $` } # ` # hack to fix syntax highlighting
|
57
|
+
preview = candidates.detect { |candidate| ActionMailer::Preview.exists?(candidate) }
|
58
|
+
|
59
|
+
if preview
|
60
|
+
@preview = ActionMailer::Preview.find(preview)
|
61
|
+
else
|
62
|
+
raise AbstractController::ActionNotFound, "Mailer preview '#{params[:path]}' not found"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_preferred_part(*formats) # :doc:
|
67
|
+
formats.each do |format|
|
68
|
+
if part = @email.find_first_mime_type(format)
|
69
|
+
return part
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
if formats.any? { |f| @email.mime_type == f }
|
74
|
+
@email
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_part(format) # :doc:
|
79
|
+
if part = @email.find_first_mime_type(format)
|
80
|
+
part
|
81
|
+
elsif @email.mime_type == format
|
82
|
+
@email
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def part_query(mime_type)
|
87
|
+
request.query_parameters.merge(part: mime_type).to_query
|
88
|
+
end
|
89
|
+
|
90
|
+
def locale_query(locale)
|
91
|
+
request.query_parameters.merge(locale: locale).to_query
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_locale
|
95
|
+
I18n.locale = params[:locale] || I18n.default_locale
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
require "aws-sdk-s3"
|
3
|
+
require "cfnresponse"
|
4
|
+
include Cfnresponse
|
5
|
+
|
6
|
+
def lambda_handler(event:, context:)
|
7
|
+
# Print out debugging info immediately just in case
|
8
|
+
puts "event: #{json_pretty(event)}"
|
9
|
+
puts "context: #{json_pretty(context)}"
|
10
|
+
|
11
|
+
if %w[Create Update].include?(event['RequestType'])
|
12
|
+
properties = event["ResourceProperties"].dup
|
13
|
+
# After deleting ServiceToken, the rest of the values is the bucket configuration properties.
|
14
|
+
properties.delete("ServiceToken")
|
15
|
+
configurator = BucketConfigurator.new
|
16
|
+
configurator.put(properties)
|
17
|
+
end
|
18
|
+
|
19
|
+
send_response(event, context, "SUCCESS")
|
20
|
+
|
21
|
+
# We rescue all exceptions and send an message to CloudFormation so we dont have to
|
22
|
+
# wait for over an hour for the stack operation to timeout and rollback.
|
23
|
+
rescue Exception => e
|
24
|
+
puts e.message
|
25
|
+
puts e.backtrace
|
26
|
+
sleep 10 # a little time for logs to be sent to CloudWatch
|
27
|
+
send_response(event, context, "FAILED")
|
28
|
+
end
|
29
|
+
|
30
|
+
########################################################
|
31
|
+
|
32
|
+
class BucketConfigurator
|
33
|
+
def put(props={})
|
34
|
+
# all props including bucket gets passed from the Custom::S3BucketConfiguration resource
|
35
|
+
props = props.deep_transform_keys { |k| k.to_s.underscore.to_sym }
|
36
|
+
puts "props: #{JSON.dump(props)}"
|
37
|
+
s3.put_bucket_notification_configuration(props)
|
38
|
+
end
|
39
|
+
|
40
|
+
def s3
|
41
|
+
@s3 ||= Aws::S3::Client.new
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html><head>
|
3
|
+
<meta name="viewport" content="width=device-width" />
|
4
|
+
<style type="text/css">
|
5
|
+
html, body, iframe {
|
6
|
+
height: 100%;
|
7
|
+
}
|
8
|
+
|
9
|
+
body {
|
10
|
+
margin: 0;
|
11
|
+
}
|
12
|
+
|
13
|
+
header {
|
14
|
+
width: 100%;
|
15
|
+
padding: 10px 0 0 0;
|
16
|
+
margin: 0;
|
17
|
+
background: white;
|
18
|
+
font: 12px "Lucida Grande", sans-serif;
|
19
|
+
border-bottom: 1px solid #dedede;
|
20
|
+
overflow: hidden;
|
21
|
+
}
|
22
|
+
|
23
|
+
dl {
|
24
|
+
margin: 0 0 10px 0;
|
25
|
+
padding: 0;
|
26
|
+
}
|
27
|
+
|
28
|
+
dt {
|
29
|
+
width: 80px;
|
30
|
+
padding: 1px;
|
31
|
+
float: left;
|
32
|
+
clear: left;
|
33
|
+
text-align: right;
|
34
|
+
color: #7f7f7f;
|
35
|
+
}
|
36
|
+
|
37
|
+
dd {
|
38
|
+
margin-left: 90px; /* 80px + 10px */
|
39
|
+
padding: 1px;
|
40
|
+
}
|
41
|
+
|
42
|
+
dd:empty:before {
|
43
|
+
content: "\00a0"; //
|
44
|
+
}
|
45
|
+
|
46
|
+
iframe {
|
47
|
+
border: 0;
|
48
|
+
width: 100%;
|
49
|
+
}
|
50
|
+
</style>
|
51
|
+
</head>
|
52
|
+
|
53
|
+
<body>
|
54
|
+
<header>
|
55
|
+
<dl>
|
56
|
+
<% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %>
|
57
|
+
<dt>SMTP-From:</dt>
|
58
|
+
<dd><%= @email.smtp_envelope_from %></dd>
|
59
|
+
<% end %>
|
60
|
+
|
61
|
+
<% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %>
|
62
|
+
<dt>SMTP-To:</dt>
|
63
|
+
<dd><%= @email.smtp_envelope_to %></dd>
|
64
|
+
<% end %>
|
65
|
+
|
66
|
+
<dt>From:</dt>
|
67
|
+
<dd><%= @email.header['from'] %></dd>
|
68
|
+
|
69
|
+
<% if @email.reply_to %>
|
70
|
+
<dt>Reply-To:</dt>
|
71
|
+
<dd><%= @email.header['reply-to'] %></dd>
|
72
|
+
<% end %>
|
73
|
+
|
74
|
+
<dt>To:</dt>
|
75
|
+
<dd><%= @email.header['to'] %></dd>
|
76
|
+
|
77
|
+
<% if @email.cc %>
|
78
|
+
<dt>CC:</dt>
|
79
|
+
<dd><%= @email.header['cc'] %></dd>
|
80
|
+
<% end %>
|
81
|
+
|
82
|
+
<dt>Date:</dt>
|
83
|
+
<dd><%= Time.current.rfc2822 %></dd>
|
84
|
+
|
85
|
+
<dt>Subject:</dt>
|
86
|
+
<dd><strong><%= @email.subject %></strong></dd>
|
87
|
+
|
88
|
+
<% unless @email.attachments.nil? || @email.attachments.empty? %>
|
89
|
+
<dt>Attachments:</dt>
|
90
|
+
<dd>
|
91
|
+
<% @email.attachments.each do |a| %>
|
92
|
+
<% filename = a.respond_to?(:original_filename) ? a.original_filename : a.filename %>
|
93
|
+
<%= link_to filename, "data:application/octet-stream;charset=utf-8;base64,#{Base64.encode64(a.body.to_s)}", download: filename %>
|
94
|
+
<% end %>
|
95
|
+
</dd>
|
96
|
+
<% end %>
|
97
|
+
|
98
|
+
<dt>Format:</dt>
|
99
|
+
<% if @email.multipart? %>
|
100
|
+
<dd>
|
101
|
+
<select id="part" onchange="refreshBody(false);">
|
102
|
+
<option <%= request.format == Mime[:html] ? 'selected' : '' %> value="<%= part_query('text/html') %>">View as HTML email</option>
|
103
|
+
<option <%= request.format == Mime[:text] ? 'selected' : '' %> value="<%= part_query('text/plain') %>">View as plain-text email</option>
|
104
|
+
</select>
|
105
|
+
</dd>
|
106
|
+
<% else %>
|
107
|
+
<dd id="mime_type" data-mime-type="<%= part_query(@email.mime_type) %>"><%= @email.mime_type == 'text/html' ? 'HTML email' : 'plain-text email' %></dd>
|
108
|
+
<% end %>
|
109
|
+
|
110
|
+
<% if I18n.available_locales.count > 1 %>
|
111
|
+
<dt>Locale:</dt>
|
112
|
+
<dd>
|
113
|
+
<select id="locale" onchange="refreshBody(true);">
|
114
|
+
<% I18n.available_locales.each do |locale| %>
|
115
|
+
<option <%= I18n.locale == locale ? 'selected' : '' %> value="<%= locale_query(locale) %>"><%= locale %></option>
|
116
|
+
<% end %>
|
117
|
+
</select>
|
118
|
+
</dd>
|
119
|
+
<% end %>
|
120
|
+
</dl>
|
121
|
+
</header>
|
122
|
+
|
123
|
+
<iframe seamless name="messageBody" src="?part=<%= part_query(@part.mime_type) %>"></iframe>
|
124
|
+
|
125
|
+
<script>
|
126
|
+
function refreshBody(reload) {
|
127
|
+
var part_select = document.querySelector('select#part');
|
128
|
+
var locale_select = document.querySelector('select#locale');
|
129
|
+
var iframe = document.getElementsByName('messageBody')[0];
|
130
|
+
var part_param = part_select ?
|
131
|
+
part_select.options[part_select.selectedIndex].value :
|
132
|
+
document.querySelector('#mime_type').dataset.mimeType;
|
133
|
+
var fresh_location = '?part=' + part_param;
|
134
|
+
var doc = iframe.contentWindow.document;
|
135
|
+
|
136
|
+
doc.open();
|
137
|
+
doc.write("Loading...");
|
138
|
+
doc.close();
|
139
|
+
|
140
|
+
iframe.contentWindow.location = fresh_location;
|
141
|
+
}
|
142
|
+
</script>
|
143
|
+
|
144
|
+
</body>
|
145
|
+
</html>
|