postmortem 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -0
- data/README.md +61 -10
- data/doc/screenshot.png +0 -0
- data/layout/default.html.erb +69 -41
- data/layout/dependencies.css +11 -0
- data/layout/dependencies.js +14 -0
- data/layout/headers_template.html +35 -0
- data/layout/index.html.erb +28 -0
- data/layout/layout.css +160 -0
- data/layout/layout.js +248 -0
- data/lib/postmortem.rb +28 -15
- data/lib/postmortem/adapters.rb +2 -0
- data/lib/postmortem/adapters/action_mailer.rb +46 -7
- data/lib/postmortem/adapters/base.rb +32 -6
- data/lib/postmortem/adapters/mail.rb +21 -0
- data/lib/postmortem/adapters/pony.rb +31 -0
- data/lib/postmortem/configuration.rb +35 -0
- data/lib/postmortem/delivery.rb +22 -11
- data/lib/postmortem/index.rb +60 -0
- data/lib/postmortem/layout.rb +87 -4
- data/lib/postmortem/{action_mailer.rb → plugins/action_mailer.rb} +0 -0
- data/lib/postmortem/plugins/mail.rb +11 -0
- data/lib/postmortem/plugins/pony.rb +17 -0
- data/lib/postmortem/version.rb +1 -1
- data/postmortem.gemspec +1 -0
- metadata +36 -8
- data/Gemfile.lock +0 -126
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Postmortem
|
4
|
+
module Adapters
|
5
|
+
# Mail adapter.
|
6
|
+
class Mail < Base
|
7
|
+
private
|
8
|
+
|
9
|
+
def adapted
|
10
|
+
%i[from reply_to to cc bcc subject]
|
11
|
+
.map { |field| [field, @data.public_send(field)] }
|
12
|
+
.to_h
|
13
|
+
.merge({ text_body: @data.text_part, html_body: @data.html_part })
|
14
|
+
end
|
15
|
+
|
16
|
+
def mail
|
17
|
+
@mail ||= @data
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Postmortem
|
4
|
+
module Adapters
|
5
|
+
# Pony adapter.
|
6
|
+
class Pony < Base
|
7
|
+
private
|
8
|
+
|
9
|
+
def adapted
|
10
|
+
{
|
11
|
+
from: mail.from,
|
12
|
+
reply_to: mail.reply_to,
|
13
|
+
to: mail.to,
|
14
|
+
cc: mail.cc,
|
15
|
+
bcc: mail.bcc,
|
16
|
+
subject: mail.subject,
|
17
|
+
text_body: @data[:body],
|
18
|
+
html_body: @data[:html_body]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def mail
|
23
|
+
@mail ||= ::Mail.new(@data.select { |key| keys.include?(key) })
|
24
|
+
end
|
25
|
+
|
26
|
+
def keys
|
27
|
+
%i[from reply_to to cc bcc subject text_body html_body]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Postmortem
|
4
|
+
# Provides interface for configuring Postmortem and implements sensible defaults.
|
5
|
+
class Configuration
|
6
|
+
attr_writer :colorize, :mail_skip_delivery
|
7
|
+
attr_accessor :log_path
|
8
|
+
|
9
|
+
def colorize
|
10
|
+
defined?(@colorize) ? @colorize : true
|
11
|
+
end
|
12
|
+
|
13
|
+
def preview_directory=(val)
|
14
|
+
@preview_directory = Pathname.new(val)
|
15
|
+
end
|
16
|
+
|
17
|
+
def layout=(val)
|
18
|
+
@layout = Pathname.new(val)
|
19
|
+
end
|
20
|
+
|
21
|
+
def layout
|
22
|
+
default = File.expand_path(File.join(__dir__, '..', '..', 'layout', 'default'))
|
23
|
+
path = Pathname.new(@layout || default)
|
24
|
+
path.extname.empty? ? path.sub_ext('.html.erb') : path
|
25
|
+
end
|
26
|
+
|
27
|
+
def preview_directory
|
28
|
+
@preview_directory ||= Pathname.new(File.join(Dir.tmpdir, 'postmortem'))
|
29
|
+
end
|
30
|
+
|
31
|
+
def mail_skip_delivery
|
32
|
+
defined?(@mail_skip_delivery) ? @mail_skip_delivery : true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/postmortem/delivery.rb
CHANGED
@@ -3,32 +3,43 @@
|
|
3
3
|
module Postmortem
|
4
4
|
# Abstraction of an email delivery. Capable of writing email HTML body to disk.
|
5
5
|
class Delivery
|
6
|
-
attr_reader :path
|
6
|
+
attr_reader :path, :index_path
|
7
7
|
|
8
|
-
def initialize(
|
9
|
-
@
|
10
|
-
@path = Postmortem.
|
8
|
+
def initialize(mail)
|
9
|
+
@mail = mail
|
10
|
+
@path = Postmortem.config.preview_directory.join('emails.html')
|
11
|
+
@index_path = Postmortem.config.preview_directory.join('index.html')
|
11
12
|
end
|
12
13
|
|
13
14
|
def record
|
14
15
|
path.parent.mkpath
|
15
|
-
|
16
|
+
content = Layout.new(@mail).content
|
17
|
+
path.write(content)
|
18
|
+
index_path.write(index.content)
|
16
19
|
end
|
17
20
|
|
18
21
|
private
|
19
22
|
|
20
|
-
def
|
21
|
-
|
23
|
+
def index
|
24
|
+
@index ||= Index.new(index_path, path, timestamp, @mail)
|
22
25
|
end
|
23
26
|
|
24
27
|
def timestamp
|
25
|
-
Time.now
|
28
|
+
@timestamp ||= Time.now
|
26
29
|
end
|
27
30
|
|
28
|
-
def
|
29
|
-
|
31
|
+
def token
|
32
|
+
SecureRandom.hex(4)
|
33
|
+
end
|
34
|
+
|
35
|
+
def subject
|
36
|
+
return 'no-subject' if @mail.subject.nil? || @mail.subject.empty?
|
30
37
|
|
31
|
-
@
|
38
|
+
@mail.subject
|
39
|
+
end
|
40
|
+
|
41
|
+
def safe_subject
|
42
|
+
subject.tr(' ', '_').split('').select { |char| safe_chars.include?(char) }.join
|
32
43
|
end
|
33
44
|
|
34
45
|
def safe_chars
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Postmortem
|
4
|
+
# Generates and parses an index of previously-sent emails.
|
5
|
+
class Index
|
6
|
+
def initialize(index_path, mail_path, timestamp, mail)
|
7
|
+
@index_path = index_path
|
8
|
+
@mail_path = mail_path
|
9
|
+
@timestamp = timestamp.iso8601
|
10
|
+
@mail = mail
|
11
|
+
end
|
12
|
+
|
13
|
+
def content
|
14
|
+
mail_path = @mail_path
|
15
|
+
ERB.new(File.read(template_path), nil, '-').result(binding)
|
16
|
+
end
|
17
|
+
|
18
|
+
def size
|
19
|
+
encoded_index.size
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def encoded_index
|
25
|
+
return [encoded_mail] unless @index_path.file?
|
26
|
+
|
27
|
+
@encoded_index ||= [encoded_mail] + lines[index(:start)..index(:end)]
|
28
|
+
end
|
29
|
+
|
30
|
+
def encoded_mail
|
31
|
+
Base64.encode64(mail_data.to_json).split("\n").join
|
32
|
+
end
|
33
|
+
|
34
|
+
def mail_data
|
35
|
+
{
|
36
|
+
subject: @mail.subject || '(no subject)',
|
37
|
+
timestamp: @timestamp,
|
38
|
+
path: @mail_path,
|
39
|
+
content: @mail.serializable
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def lines
|
44
|
+
@lines ||= @index_path.read.split("\n")
|
45
|
+
end
|
46
|
+
|
47
|
+
def index(position)
|
48
|
+
offset = { start: 1, end: -1 }.fetch(position)
|
49
|
+
lines.index(marker(position)) + offset
|
50
|
+
end
|
51
|
+
|
52
|
+
def marker(position)
|
53
|
+
"### INDEX #{position.to_s.upcase}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def template_path
|
57
|
+
File.expand_path(File.join(__dir__, '..', '..', 'layout', 'index.html.erb'))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/postmortem/layout.rb
CHANGED
@@ -3,13 +3,96 @@
|
|
3
3
|
module Postmortem
|
4
4
|
# Wraps provided body in an enclosing layout for presentation purposes.
|
5
5
|
class Layout
|
6
|
-
def initialize(
|
7
|
-
@
|
6
|
+
def initialize(mail)
|
7
|
+
@mail = mail
|
8
|
+
end
|
9
|
+
|
10
|
+
def format_email_array(array)
|
11
|
+
array&.map { |email| %(<a href="mailto:#{email}">#{email}</a>) }&.join(', ')
|
8
12
|
end
|
9
13
|
|
10
14
|
def content
|
11
|
-
mail = @
|
12
|
-
|
15
|
+
mail = @mail
|
16
|
+
mail.html_body = with_inlined_images(mail.html_body) if defined?(Nokogiri)
|
17
|
+
ERB.new(Postmortem.config.layout.read).result(binding)
|
18
|
+
end
|
19
|
+
|
20
|
+
def styles
|
21
|
+
default_layout_directory.join('layout.css').read
|
22
|
+
end
|
23
|
+
|
24
|
+
def javascript
|
25
|
+
default_layout_directory.join('layout.js').read
|
26
|
+
end
|
27
|
+
|
28
|
+
def css_dependencies
|
29
|
+
default_layout_directory.join('dependencies.css').read
|
30
|
+
end
|
31
|
+
|
32
|
+
def javascript_dependencies
|
33
|
+
default_layout_directory.join('dependencies.js').read
|
34
|
+
end
|
35
|
+
|
36
|
+
def headers_template
|
37
|
+
default_layout_directory.join('headers_template.html').read
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def default_layout_directory
|
43
|
+
Postmortem.root.join('layout')
|
44
|
+
end
|
45
|
+
|
46
|
+
def with_inlined_images(body)
|
47
|
+
parsed = Nokogiri::HTML.parse(body)
|
48
|
+
parsed.css('img').each do |img|
|
49
|
+
uri = try_uri(img['src'])
|
50
|
+
next unless local_file?(uri)
|
51
|
+
|
52
|
+
path = located_image(uri)
|
53
|
+
img['src'] = encoded_image(path) unless path.nil?
|
54
|
+
end
|
55
|
+
parsed.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def local_file?(uri)
|
59
|
+
return false if uri.nil?
|
60
|
+
return true if uri.host.nil?
|
61
|
+
return true if /^www\.example\.[a-z]+$/.match(uri.host)
|
62
|
+
return true if %w[127.0.0.1 localhost].include?(uri.host)
|
63
|
+
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
def try_uri(uri)
|
68
|
+
URI(uri)
|
69
|
+
rescue URI::InvalidURIError
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def located_image(uri)
|
74
|
+
path = uri.path.partition('/').last
|
75
|
+
common_locations.each do |location|
|
76
|
+
full_path = location.join(path)
|
77
|
+
next unless full_path.file?
|
78
|
+
|
79
|
+
return full_path
|
80
|
+
end
|
81
|
+
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def encoded_image(path)
|
86
|
+
"data:#{mime_type(path)};base64,#{Base64.encode64(path.read)}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def common_locations
|
90
|
+
['public/assets', 'app/assets/images'].map { |path| Pathname.new(path) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def mime_type(path)
|
94
|
+
extension = path.extname.partition('.').last
|
95
|
+
extension == 'jpg' ? 'jpeg' : extension
|
13
96
|
end
|
14
97
|
end
|
15
98
|
end
|
File without changes
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Mail::SMTP.class_eval do
|
4
|
+
alias_method :_original_deliver!, :deliver!
|
5
|
+
|
6
|
+
def deliver!(mail)
|
7
|
+
result = _original_deliver!(mail) unless Postmortem.config.mail_skip_delivery
|
8
|
+
Postmortem.record_delivery(Postmortem::Adapters::Mail.new(mail))
|
9
|
+
result
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Pony monkey-patch.
|
4
|
+
module Pony
|
5
|
+
class << self
|
6
|
+
alias _original_mail mail
|
7
|
+
|
8
|
+
def mail(options)
|
9
|
+
# SMTP delivery is handled by Mail plugin further down the stack
|
10
|
+
return _original_mail(options) if options[:via].to_s == 'smtp'
|
11
|
+
|
12
|
+
result = _original_mail(options) unless Postmortem.config.mail_skip_delivery
|
13
|
+
Postmortem.record_delivery(Postmortem::Adapters::Pony.new(options))
|
14
|
+
result
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/postmortem/version.rb
CHANGED
data/postmortem.gemspec
CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_runtime_dependency 'mail', '~> 2.7'
|
29
29
|
|
30
30
|
spec.add_development_dependency 'actionmailer', '~> 6.0'
|
31
|
+
spec.add_development_dependency 'pony', '~> 1.13'
|
31
32
|
spec.add_development_dependency 'rspec', '~> 3.9'
|
32
33
|
spec.add_development_dependency 'rspec-its', '~> 1.3'
|
33
34
|
spec.add_development_dependency 'rubocop', '~> 0.88.0'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: postmortem
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bob Farrell
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mail
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '6.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pony
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.13'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.13'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -118,22 +132,35 @@ files:
|
|
118
132
|
- ".gitignore"
|
119
133
|
- ".rspec"
|
120
134
|
- ".rubocop.yml"
|
135
|
+
- ".ruby-version"
|
121
136
|
- Gemfile
|
122
|
-
- Gemfile.lock
|
123
137
|
- LICENSE.txt
|
124
138
|
- Makefile
|
125
139
|
- README.md
|
126
140
|
- Rakefile
|
127
141
|
- bin/console
|
128
142
|
- bin/setup
|
143
|
+
- doc/screenshot.png
|
129
144
|
- layout/default.html.erb
|
145
|
+
- layout/dependencies.css
|
146
|
+
- layout/dependencies.js
|
147
|
+
- layout/headers_template.html
|
148
|
+
- layout/index.html.erb
|
149
|
+
- layout/layout.css
|
150
|
+
- layout/layout.js
|
130
151
|
- lib/postmortem.rb
|
131
|
-
- lib/postmortem/action_mailer.rb
|
132
152
|
- lib/postmortem/adapters.rb
|
133
153
|
- lib/postmortem/adapters/action_mailer.rb
|
134
154
|
- lib/postmortem/adapters/base.rb
|
155
|
+
- lib/postmortem/adapters/mail.rb
|
156
|
+
- lib/postmortem/adapters/pony.rb
|
157
|
+
- lib/postmortem/configuration.rb
|
135
158
|
- lib/postmortem/delivery.rb
|
159
|
+
- lib/postmortem/index.rb
|
136
160
|
- lib/postmortem/layout.rb
|
161
|
+
- lib/postmortem/plugins/action_mailer.rb
|
162
|
+
- lib/postmortem/plugins/mail.rb
|
163
|
+
- lib/postmortem/plugins/pony.rb
|
137
164
|
- lib/postmortem/version.rb
|
138
165
|
- postmortem.gemspec
|
139
166
|
homepage: https://github.com/bobf/postmortem
|
@@ -143,7 +170,7 @@ metadata:
|
|
143
170
|
homepage_uri: https://github.com/bobf/postmortem
|
144
171
|
source_code_uri: https://github.com/bobf/postmortem
|
145
172
|
changelog_uri: https://github.com/bobf/postmortem/blob/master/README.md
|
146
|
-
post_install_message:
|
173
|
+
post_install_message:
|
147
174
|
rdoc_options: []
|
148
175
|
require_paths:
|
149
176
|
- lib
|
@@ -158,8 +185,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
185
|
- !ruby/object:Gem::Version
|
159
186
|
version: '0'
|
160
187
|
requirements: []
|
161
|
-
|
162
|
-
|
188
|
+
rubyforge_project:
|
189
|
+
rubygems_version: 2.7.6
|
190
|
+
signing_key:
|
163
191
|
specification_version: 4
|
164
192
|
summary: Development HTML Email Inspection Tool
|
165
193
|
test_files: []
|