postmortem 0.2.0 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,7 +6,7 @@ require 'postmortem/adapters/mail'
6
6
  require 'postmortem/adapters/pony'
7
7
 
8
8
  module Postmortem
9
- # Adapters for various email senders (e.g. ActionMailer).
9
+ # Adapters for various email senders (e.g. Mail, Pony).
10
10
  module Adapters
11
11
  end
12
12
  end
@@ -7,53 +7,18 @@ module Postmortem
7
7
  private
8
8
 
9
9
  def adapted
10
- {
11
- from: mail.from,
12
- reply_to: mail.reply_to,
13
- to: mail.to,
14
- cc: mail.cc,
15
- bcc: normalized_bcc,
16
- subject: mail.subject,
17
- text_body: text_part,
18
- html_body: html_part
19
- }
20
- end
21
-
22
- def text_part
23
- return nil unless text?
24
- return mail.body.decoded unless mail.text_part
25
-
26
- mail.text_part.decoded
27
- end
28
-
29
- def html_part
30
- return nil unless html?
31
- return mail.body.decoded unless mail.html_part
32
-
33
- mail.html_part.decoded
34
- end
35
-
36
- def mail
37
- @mail ||= ::Mail.new(@data[:mail])
10
+ %i[from reply_to to cc subject message_id]
11
+ .map { |field| [field, mail.public_send(field)] }
12
+ .to_h
13
+ .merge({ text_body: text_part, html_body: html_part, bcc: normalized_bcc })
38
14
  end
39
15
 
40
16
  def normalized_bcc
41
17
  ::Mail.new(to: @data[:bcc]).to
42
18
  end
43
19
 
44
- def text?
45
- return true unless mail.has_content_type?
46
- return true if mail.content_type.include?('text/plain')
47
- return true if mail.multipart? && mail.text_part
48
-
49
- false
50
- end
51
-
52
- def html?
53
- return true if mail.has_content_type? && mail.content_type.include?('text/html')
54
- return true if mail.multipart? && mail.html_part
55
-
56
- false
20
+ def mail
21
+ @mail ||= ::Mail.new(@data[:mail])
57
22
  end
58
23
  end
59
24
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Postmortem
4
4
  module Adapters
5
- FIELDS = %i[from reply_to to cc bcc subject text_body html_body].freeze
5
+ FIELDS = %i[from reply_to to cc bcc subject text_body html_body message_id].freeze
6
6
 
7
7
  # Base interface implementation for all Postmortem adapters.
8
8
  class Base
@@ -16,7 +16,11 @@ module Postmortem
16
16
  end
17
17
 
18
18
  def serializable
19
- FIELDS.map { |field| [camelize(field.to_s), public_send(field)] }.to_h
19
+ (%i[id] + FIELDS).map { |field| [camelize(field.to_s), public_send(field)] }.to_h
20
+ end
21
+
22
+ def id
23
+ @id ||= SecureRandom.uuid
20
24
  end
21
25
 
22
26
  FIELDS.each do |method_name|
@@ -25,6 +29,14 @@ module Postmortem
25
29
  end
26
30
  end
27
31
 
32
+ def html_body
33
+ @adapted[:html_body].to_s
34
+ end
35
+
36
+ def text_body
37
+ @adapted[:text_body].to_s
38
+ end
39
+
28
40
  private
29
41
 
30
42
  def adapted
@@ -38,6 +50,35 @@ module Postmortem
38
50
  .map { |substring, index| index.zero? ? substring : substring.capitalize }
39
51
  .join
40
52
  end
53
+
54
+ def text_part
55
+ return nil unless text?
56
+ return mail.body.decoded unless mail.text_part
57
+
58
+ mail.text_part.decoded
59
+ end
60
+
61
+ def html_part
62
+ return nil unless html?
63
+ return mail.body.decoded unless mail.html_part
64
+
65
+ mail.html_part.decoded
66
+ end
67
+
68
+ def text?
69
+ return true unless mail.has_content_type?
70
+ return true if mail.content_type.include?('text/plain')
71
+ return true if mail.multipart? && mail.text_part
72
+
73
+ false
74
+ end
75
+
76
+ def html?
77
+ return true if mail.has_content_type? && mail.content_type.include?('text/html')
78
+ return true if mail.multipart? && mail.html_part
79
+
80
+ false
81
+ end
41
82
  end
42
83
  end
43
84
  end
@@ -7,10 +7,10 @@ module Postmortem
7
7
  private
8
8
 
9
9
  def adapted
10
- %i[from reply_to to cc bcc subject]
11
- .map { |field| [field, @data.public_send(field)] }
10
+ %i[from reply_to to cc bcc subject message_id]
11
+ .map { |field| [field, mail.public_send(field)] }
12
12
  .to_h
13
- .merge({ text_body: @data.text_part, html_body: @data.html_part })
13
+ .merge({ text_body: text_part, html_body: html_part })
14
14
  end
15
15
 
16
16
  def mail
@@ -8,14 +8,11 @@ module Postmortem
8
8
 
9
9
  def adapted
10
10
  {
11
- from: mail.from,
12
- reply_to: mail.reply_to,
13
- to: mail.to,
14
- cc: mail.cc,
15
- bcc: mail.bcc,
11
+ from: mail.from, reply_to: mail.reply_to, to: mail.to, cc: mail.cc, bcc: mail.bcc,
16
12
  subject: mail.subject,
17
13
  text_body: @data[:body],
18
- html_body: @data[:html_body]
14
+ html_body: @data[:html_body],
15
+ message_id: mail.message_id # We use a synthetic Mail instance so this is a bit useless.
19
16
  }
20
17
  end
21
18
 
@@ -3,17 +3,9 @@
3
3
  module Postmortem
4
4
  # Provides interface for configuring Postmortem and implements sensible defaults.
5
5
  class Configuration
6
- attr_writer :colorize, :timestamp, :mail_skip_delivery, :token
6
+ attr_writer :colorize, :mail_skip_delivery
7
7
  attr_accessor :log_path
8
8
 
9
- def timestamp
10
- defined?(@timestamp) ? @timestamp : true
11
- end
12
-
13
- def token
14
- defined?(@token) ? @token : true
15
- end
16
-
17
9
  def colorize
18
10
  defined?(@colorize) ? @colorize : true
19
11
  end
@@ -7,33 +7,27 @@ module Postmortem
7
7
 
8
8
  def initialize(mail)
9
9
  @mail = mail
10
- @path = Postmortem.config.preview_directory.join(filename)
11
- @index_path = Postmortem.config.preview_directory.join('index.html')
10
+ @path = Postmortem.config.preview_directory.join('index.html')
11
+ @index_path = Postmortem.config.preview_directory.join('postmortem_index.html')
12
+ @identity_path = Postmortem.config.preview_directory.join('postmortem_identity.html')
12
13
  end
13
14
 
14
15
  def record
15
16
  path.parent.mkpath
16
17
  content = Layout.new(@mail).content
17
18
  path.write(content)
18
- index_path.write(Index.new(index_path, path, timestamp, @mail).content)
19
+ index_path.write(index.content)
20
+ @identity_path.write(identity.content)
19
21
  end
20
22
 
21
23
  private
22
24
 
23
- def filename
24
- format = '%Y-%m-%d_%H-%M-%S'
25
- timestamp_chunk = Postmortem.config.timestamp ? "#{timestamp.strftime(format)}__" : nil
26
- token_chunk = Postmortem.config.token ? "#{token}__" : nil
27
-
28
- "#{timestamp_chunk}#{token_chunk}#{safe_subject}.html"
29
- end
30
-
31
- def timestamp
32
- @timestamp ||= Time.now
25
+ def index
26
+ @index ||= Index.new(index_path, path, @mail)
33
27
  end
34
28
 
35
- def token
36
- SecureRandom.hex(4)
29
+ def identity
30
+ @identity ||= Identity.new
37
31
  end
38
32
 
39
33
  def subject
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postmortem
4
+ # Provides an HTML document that announces a unique ID to a parent page via JS message events.
5
+ class Identity
6
+ def content
7
+ ERB.new(File.read(path), nil, '-').result(binding)
8
+ end
9
+
10
+ private
11
+
12
+ def uuid
13
+ @uuid ||= SecureRandom.uuid
14
+ end
15
+
16
+ def path
17
+ File.expand_path(File.join(__dir__, '..', '..', 'layout', 'postmortem_identity.html.erb'))
18
+ end
19
+ end
20
+ end
@@ -3,10 +3,9 @@
3
3
  module Postmortem
4
4
  # Generates and parses an index of previously-sent emails.
5
5
  class Index
6
- def initialize(index_path, mail_path, timestamp, mail)
6
+ def initialize(index_path, mail_path, mail)
7
7
  @index_path = index_path
8
8
  @mail_path = mail_path
9
- @timestamp = timestamp.iso8601
10
9
  @mail = mail
11
10
  end
12
11
 
@@ -15,23 +14,36 @@ module Postmortem
15
14
  ERB.new(File.read(template_path), nil, '-').result(binding)
16
15
  end
17
16
 
17
+ def size
18
+ encoded_index.size
19
+ end
20
+
18
21
  private
19
22
 
23
+ def uuid
24
+ @uuid ||= SecureRandom.uuid
25
+ end
26
+
27
+ def timestamp
28
+ Time.now.iso8601
29
+ end
30
+
20
31
  def encoded_index
21
32
  return [encoded_mail] unless @index_path.file?
22
33
 
23
- [encoded_mail] + lines[index(:start)..index(:end)]
34
+ @encoded_index ||= [encoded_mail] + lines[index(:start)..index(:end)]
24
35
  end
25
36
 
26
37
  def encoded_mail
27
- Base64.urlsafe_encode64(mail_data.to_json)
38
+ Base64.encode64(mail_data.to_json).split("\n").join
28
39
  end
29
40
 
30
41
  def mail_data
31
42
  {
32
43
  subject: @mail.subject || '(no subject)',
33
- timestamp: @timestamp,
44
+ timestamp: timestamp,
34
45
  path: @mail_path,
46
+ id: @mail.id,
35
47
  content: @mail.serializable
36
48
  }
37
49
  end
@@ -50,7 +62,7 @@ module Postmortem
50
62
  end
51
63
 
52
64
  def template_path
53
- File.expand_path(File.join(__dir__, '..', '..', 'layout', 'index.html.erb'))
65
+ File.expand_path(File.join(__dir__, '..', '..', 'layout', 'postmortem_index.html.erb'))
54
66
  end
55
67
  end
56
68
  end
@@ -25,10 +25,22 @@ module Postmortem
25
25
  default_layout_directory.join('layout.js').read
26
26
  end
27
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
+
28
36
  def headers_template
29
37
  default_layout_directory.join('headers_template.html').read
30
38
  end
31
39
 
40
+ def favicon_b64
41
+ default_layout_directory.join('favicon.b64').read
42
+ end
43
+
32
44
  private
33
45
 
34
46
  def default_layout_directory
@@ -38,7 +50,7 @@ module Postmortem
38
50
  def with_inlined_images(body)
39
51
  parsed = Nokogiri::HTML.parse(body)
40
52
  parsed.css('img').each do |img|
41
- uri = URI(img['src'])
53
+ uri = try_uri(img['src'])
42
54
  next unless local_file?(uri)
43
55
 
44
56
  path = located_image(uri)
@@ -48,6 +60,7 @@ module Postmortem
48
60
  end
49
61
 
50
62
  def local_file?(uri)
63
+ return false if uri.nil?
51
64
  return true if uri.host.nil?
52
65
  return true if /^www\.example\.[a-z]+$/.match(uri.host)
53
66
  return true if %w[127.0.0.1 localhost].include?(uri.host)
@@ -55,6 +68,12 @@ module Postmortem
55
68
  false
56
69
  end
57
70
 
71
+ def try_uri(uri)
72
+ URI(uri)
73
+ rescue URI::InvalidURIError
74
+ nil
75
+ end
76
+
58
77
  def located_image(uri)
59
78
  path = uri.path.partition('/').last
60
79
  common_locations.each do |location|
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  ActiveSupport::Notifications.subscribe 'deliver.action_mailer' do |*args|
4
+ delivery_method = Rails.try(:application)
5
+ &.try(:config)
6
+ &.try(:action_mailer)
7
+ &.try(:delivery_method)
8
+ next if delivery_method.nil?
9
+ next if %i[sendmail smtp].include?(delivery_method&.to_sym) # Delegate to Mail plugin.
10
+
4
11
  Postmortem.record_delivery(Postmortem::Adapters::ActionMailer.new(args.extract_options!))
5
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Postmortem
4
- VERSION = '0.2.0'
4
+ VERSION = '0.2.6'
5
5
  end
data/postmortem.gemspec CHANGED
@@ -12,14 +12,14 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'Preview HTML emails in your browser during development'
13
13
  spec.homepage = 'https://github.com/bobf/postmortem'
14
14
  spec.license = 'MIT'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
16
16
 
17
17
  spec.metadata['homepage_uri'] = spec.homepage
18
18
  spec.metadata['source_code_uri'] = spec.homepage
19
19
  spec.metadata['changelog_uri'] = 'https://github.com/bobf/postmortem/blob/master/README.md'
20
20
 
21
21
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|doc)/}) }
23
23
  end
24
24
  spec.bindir = 'bin'
25
25
  spec.executables = []
@@ -28,10 +28,13 @@ 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 'devpack', '~> 0.3.2'
32
+ spec.add_development_dependency 'faker', '~> 2.17'
31
33
  spec.add_development_dependency 'pony', '~> 1.13'
32
34
  spec.add_development_dependency 'rspec', '~> 3.9'
33
35
  spec.add_development_dependency 'rspec-its', '~> 1.3'
34
- spec.add_development_dependency 'rubocop', '~> 0.88.0'
36
+ spec.add_development_dependency 'rubocop', '~> 1.10'
37
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.2'
35
38
  spec.add_development_dependency 'strong_versions', '~> 0.4.5'
36
39
  spec.add_development_dependency 'timecop', '~> 0.9.1'
37
40
  end
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.2.0
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-10 00:00:00.000000000 Z
11
+ date: 2021-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: devpack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.3.2
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.3.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: faker
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.17'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.17'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: pony
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +114,28 @@ dependencies:
86
114
  requirements:
87
115
  - - "~>"
88
116
  - !ruby/object:Gem::Version
89
- version: 0.88.0
117
+ version: '1.10'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.10'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2.2'
90
132
  type: :development
91
133
  prerelease: false
92
134
  version_requirements: !ruby/object:Gem::Requirement
93
135
  requirements:
94
136
  - - "~>"
95
137
  - !ruby/object:Gem::Version
96
- version: 0.88.0
138
+ version: '2.2'
97
139
  - !ruby/object:Gem::Dependency
98
140
  name: strong_versions
99
141
  requirement: !ruby/object:Gem::Requirement
@@ -140,12 +182,15 @@ files:
140
182
  - Rakefile
141
183
  - bin/console
142
184
  - bin/setup
143
- - doc/screenshot.png
144
185
  - layout/default.html.erb
186
+ - layout/dependencies.css
187
+ - layout/dependencies.js
188
+ - layout/favicon.b64
145
189
  - layout/headers_template.html
146
- - layout/index.html.erb
147
190
  - layout/layout.css
148
191
  - layout/layout.js
192
+ - layout/postmortem_identity.html.erb
193
+ - layout/postmortem_index.html.erb
149
194
  - lib/postmortem.rb
150
195
  - lib/postmortem/adapters.rb
151
196
  - lib/postmortem/adapters/action_mailer.rb
@@ -154,6 +199,7 @@ files:
154
199
  - lib/postmortem/adapters/pony.rb
155
200
  - lib/postmortem/configuration.rb
156
201
  - lib/postmortem/delivery.rb
202
+ - lib/postmortem/identity.rb
157
203
  - lib/postmortem/index.rb
158
204
  - lib/postmortem/layout.rb
159
205
  - lib/postmortem/plugins/action_mailer.rb
@@ -176,7 +222,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
176
222
  requirements:
177
223
  - - ">="
178
224
  - !ruby/object:Gem::Version
179
- version: 2.3.0
225
+ version: 2.5.0
180
226
  required_rubygems_version: !ruby/object:Gem::Requirement
181
227
  requirements:
182
228
  - - ">="
@@ -184,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
230
  version: '0'
185
231
  requirements: []
186
232
  rubyforge_project:
187
- rubygems_version: 2.7.6
233
+ rubygems_version: 2.7.6.2
188
234
  signing_key:
189
235
  specification_version: 4
190
236
  summary: Development HTML Email Inspection Tool