postmortem 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body>
4
+ <div data-uuid="<%= uuid %>" id="identity">
5
+ </div>
6
+ <script>
7
+ window.addEventListener('message', function (ev) {
8
+ const identity = document.querySelector('#identity');
9
+ const uuid = identity.dataset.uuid;
10
+ const type = 'identity';
11
+ ev.source.postMessage({ uuid, type }, '*');
12
+ });
13
+
14
+ setInterval(function () {
15
+ window.location.reload();
16
+ }, 3000);
17
+
18
+ </script>
19
+ </body>
20
+ </html>
@@ -1,7 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <body>
4
- <div data-uuid="<%= SecureRandom.uuid %>" id="index">
4
+ <div data-uuid="<%= uuid %>" id="index">
5
5
  ### INDEX START
6
6
  <% encoded_index.each do |encoded| -%>
7
7
  <%= encoded %>
@@ -13,16 +13,13 @@
13
13
  const filter = (line) => line !== '' && !line.startsWith('### INDEX');
14
14
  const index = document.querySelector('#index');
15
15
  const uuid = index.dataset.uuid;
16
+ const type = 'index';
16
17
  const mails = index.innerText
17
18
  .split('\n')
18
19
  .filter(filter)
19
20
  .map(line => JSON.parse(atob(line)));
20
- ev.source.postMessage({ uuid, mails }, '*');
21
+ ev.source.postMessage({ uuid, type, mails }, '*');
21
22
  });
22
-
23
- setInterval(function () {
24
- window.location.reload();
25
- }, 3000);
26
23
  </script>
27
24
  </body>
28
25
  </html>
data/lib/postmortem.rb CHANGED
@@ -8,12 +8,15 @@ require 'mail'
8
8
  require 'erb'
9
9
  require 'json'
10
10
  require 'cgi'
11
+ require 'digest'
12
+ require 'securerandom'
11
13
 
12
14
  require 'postmortem/version'
13
15
  require 'postmortem/adapters'
14
16
  require 'postmortem/delivery'
15
17
  require 'postmortem/layout'
16
18
  require 'postmortem/configuration'
19
+ require 'postmortem/identity'
17
20
  require 'postmortem/index'
18
21
 
19
22
  # HTML email inspection tool.
@@ -54,7 +57,7 @@ module Postmortem
54
57
  private
55
58
 
56
59
  def log_delivery(delivery)
57
- output_file.write(colorized(delivery.path.to_s) + "\n")
60
+ output_file.write("#{colorized(delivery.uri)}\n")
58
61
  output_file.flush
59
62
  end
60
63
 
@@ -65,7 +68,7 @@ module Postmortem
65
68
  end
66
69
 
67
70
  def output_file
68
- return STDOUT if config.log_path.nil?
71
+ return $stdout if config.log_path.nil?
69
72
 
70
73
  @output_file ||= File.open(config.log_path, mode: 'a')
71
74
  end
@@ -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|
@@ -46,6 +50,35 @@ module Postmortem
46
50
  .map { |substring, index| index.zero? ? substring : substring.capitalize }
47
51
  .join
48
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
49
82
  end
50
83
  end
51
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
 
@@ -7,8 +7,9 @@ module Postmortem
7
7
 
8
8
  def initialize(mail)
9
9
  @mail = mail
10
- @path = Postmortem.config.preview_directory.join('emails.html')
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
@@ -16,20 +17,21 @@ module Postmortem
16
17
  content = Layout.new(@mail).content
17
18
  path.write(content)
18
19
  index_path.write(index.content)
20
+ @identity_path.write(identity.content)
21
+ end
22
+
23
+ def uri
24
+ "file://#{path}##{@mail.id}"
19
25
  end
20
26
 
21
27
  private
22
28
 
23
29
  def index
24
- @index ||= Index.new(index_path, path, timestamp, @mail)
25
- end
26
-
27
- def timestamp
28
- @timestamp ||= Time.now
30
+ @index ||= Index.new(index_path, path, @mail)
29
31
  end
30
32
 
31
- def token
32
- SecureRandom.hex(4)
33
+ def identity
34
+ @identity ||= Identity.new
33
35
  end
34
36
 
35
37
  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
 
@@ -21,6 +20,14 @@ module Postmortem
21
20
 
22
21
  private
23
22
 
23
+ def uuid
24
+ @uuid ||= SecureRandom.uuid
25
+ end
26
+
27
+ def timestamp
28
+ Time.now.iso8601
29
+ end
30
+
24
31
  def encoded_index
25
32
  return [encoded_mail] unless @index_path.file?
26
33
 
@@ -34,8 +41,9 @@ module Postmortem
34
41
  def mail_data
35
42
  {
36
43
  subject: @mail.subject || '(no subject)',
37
- timestamp: @timestamp,
44
+ timestamp: timestamp,
38
45
  path: @mail_path,
46
+ id: @mail.id,
39
47
  content: @mail.serializable
40
48
  }
41
49
  end
@@ -54,7 +62,7 @@ module Postmortem
54
62
  end
55
63
 
56
64
  def template_path
57
- File.expand_path(File.join(__dir__, '..', '..', 'layout', 'index.html.erb'))
65
+ File.expand_path(File.join(__dir__, '..', '..', 'layout', 'postmortem_index.html.erb'))
58
66
  end
59
67
  end
60
68
  end
@@ -37,6 +37,14 @@ module Postmortem
37
37
  default_layout_directory.join('headers_template.html').read
38
38
  end
39
39
 
40
+ def favicon_b64
41
+ default_layout_directory.join('favicon.b64').read
42
+ end
43
+
44
+ def upload_url
45
+ ENV.fetch('POSTMORTEM_DELIVERY_URL', 'https://postmortem.delivery/emails')
46
+ end
47
+
40
48
  private
41
49
 
42
50
  def default_layout_directory
@@ -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.1'
4
+ VERSION = '0.3.0'
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.1
4
+ version: 0.3.0
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-12 00:00:00.000000000 Z
11
+ date: 2021-04-15 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,14 +182,15 @@ files:
140
182
  - Rakefile
141
183
  - bin/console
142
184
  - bin/setup
143
- - doc/screenshot.png
144
185
  - layout/default.html.erb
145
186
  - layout/dependencies.css
146
187
  - layout/dependencies.js
188
+ - layout/favicon.b64
147
189
  - layout/headers_template.html
148
- - layout/index.html.erb
149
190
  - layout/layout.css
150
191
  - layout/layout.js
192
+ - layout/postmortem_identity.html.erb
193
+ - layout/postmortem_index.html.erb
151
194
  - lib/postmortem.rb
152
195
  - lib/postmortem/adapters.rb
153
196
  - lib/postmortem/adapters/action_mailer.rb
@@ -156,6 +199,7 @@ files:
156
199
  - lib/postmortem/adapters/pony.rb
157
200
  - lib/postmortem/configuration.rb
158
201
  - lib/postmortem/delivery.rb
202
+ - lib/postmortem/identity.rb
159
203
  - lib/postmortem/index.rb
160
204
  - lib/postmortem/layout.rb
161
205
  - lib/postmortem/plugins/action_mailer.rb
@@ -178,7 +222,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
178
222
  requirements:
179
223
  - - ">="
180
224
  - !ruby/object:Gem::Version
181
- version: 2.3.0
225
+ version: 2.5.0
182
226
  required_rubygems_version: !ruby/object:Gem::Requirement
183
227
  requirements:
184
228
  - - ">="
@@ -186,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
230
  version: '0'
187
231
  requirements: []
188
232
  rubyforge_project:
189
- rubygems_version: 2.7.6
233
+ rubygems_version: 2.7.6.2
190
234
  signing_key:
191
235
  specification_version: 4
192
236
  summary: Development HTML Email Inspection Tool