postmortem 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []