postmortem 0.1.0 → 0.2.1

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.
@@ -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
@@ -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(adapter)
9
- @adapter = adapter
10
- @path = Postmortem.output_directory.join(filename)
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
- File.write(path, Layout.new(@adapter).content)
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 filename
21
- "#{timestamp}__#{safe_subject}.html"
23
+ def index
24
+ @index ||= Index.new(index_path, path, timestamp, @mail)
22
25
  end
23
26
 
24
27
  def timestamp
25
- Time.now.strftime('%Y-%m-%d_%H-%M-%S')
28
+ @timestamp ||= Time.now
26
29
  end
27
30
 
28
- def safe_subject
29
- return 'no-subject' if @adapter.subject.empty?
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
- @adapter.subject.tr(' ', '_').split('').select { |char| safe_chars.include?(char) }.join
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
@@ -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(adapter)
7
- @adapter = adapter
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 = @adapter
12
- ERB.new(Postmortem.layout.read).result(binding)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Postmortem
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.1'
5
5
  end
@@ -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.0
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: 2020-08-15 00:00:00.000000000 Z
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
- rubygems_version: 3.1.2
162
- signing_key:
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: []