postmortem 0.1.3 → 0.2.5

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,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>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body>
4
+ <div data-uuid="<%= uuid %>" id="index">
5
+ ### INDEX START
6
+ <% encoded_index.each do |encoded| -%>
7
+ <%= encoded %>
8
+ <% end -%>
9
+ ### INDEX END
10
+ </div>
11
+ <script>
12
+ window.addEventListener('message', function (ev) {
13
+ const filter = (line) => line !== '' && !line.startsWith('### INDEX');
14
+ const index = document.querySelector('#index');
15
+ const uuid = index.dataset.uuid;
16
+ const type = 'index';
17
+ const mails = index.innerText
18
+ .split('\n')
19
+ .filter(filter)
20
+ .map(line => JSON.parse(atob(line)));
21
+ ev.source.postMessage({ uuid, type, mails }, '*');
22
+ });
23
+ </script>
24
+ </body>
25
+ </html>
data/lib/postmortem.rb CHANGED
@@ -8,12 +8,16 @@ 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'
20
+ require 'postmortem/index'
17
21
 
18
22
  # HTML email inspection tool.
19
23
  module Postmortem
@@ -22,6 +26,10 @@ module Postmortem
22
26
  class << self
23
27
  attr_reader :config
24
28
 
29
+ def root
30
+ Pathname.new(__dir__).parent
31
+ end
32
+
25
33
  def record_delivery(mail)
26
34
  Delivery.new(mail)
27
35
  .tap(&:record)
@@ -42,10 +50,14 @@ module Postmortem
42
50
  yield @config if block_given?
43
51
  end
44
52
 
53
+ def clear_inbox
54
+ config.preview_directory.rmtree
55
+ end
56
+
45
57
  private
46
58
 
47
59
  def log_delivery(delivery)
48
- output_file.write(colorized(delivery.path.to_s) + "\n")
60
+ output_file.write("#{colorized(delivery.path.to_s)}\n")
49
61
  output_file.flush
50
62
  end
51
63
 
@@ -56,7 +68,7 @@ module Postmortem
56
68
  end
57
69
 
58
70
  def output_file
59
- return STDOUT if config.log_path.nil?
71
+ return $stdout if config.log_path.nil?
60
72
 
61
73
  @output_file ||= File.open(config.log_path, mode: 'a')
62
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,6 +2,8 @@
2
2
 
3
3
  module Postmortem
4
4
  module Adapters
5
+ FIELDS = %i[from reply_to to cc bcc subject text_body html_body message_id].freeze
6
+
5
7
  # Base interface implementation for all Postmortem adapters.
6
8
  class Base
7
9
  def initialize(data)
@@ -9,17 +11,74 @@ module Postmortem
9
11
  @adapted = adapted
10
12
  end
11
13
 
12
- %i[from reply_to to cc bcc subject text_body html_body].each do |method_name|
14
+ def html_body=(val)
15
+ @adapted[:html_body] = val
16
+ end
17
+
18
+ def serializable
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
24
+ end
25
+
26
+ FIELDS.each do |method_name|
13
27
  define_method method_name do
14
28
  @adapted[method_name]
15
29
  end
16
30
  end
17
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
+
18
40
  private
19
41
 
20
42
  def adapted
21
43
  raise NotImplementedError, 'Adapter child class must implement #adapted'
22
44
  end
45
+
46
+ def camelize(string)
47
+ string
48
+ .split('_')
49
+ .each_with_index
50
+ .map { |substring, index| index.zero? ? substring : substring.capitalize }
51
+ .join
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
23
82
  end
24
83
  end
25
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,13 +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
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
9
  def colorize
14
10
  defined?(@colorize) ? @colorize : true
15
11
  end
@@ -3,34 +3,41 @@
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.config.preview_directory.join(filename)
8
+ def initialize(mail)
9
+ @mail = mail
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')
11
13
  end
12
14
 
13
15
  def record
14
16
  path.parent.mkpath
15
- File.write(path, Layout.new(@adapter).content)
17
+ content = Layout.new(@mail).content
18
+ path.write(content)
19
+ index_path.write(index.content)
20
+ @identity_path.write(identity.content)
16
21
  end
17
22
 
18
23
  private
19
24
 
20
- def filename
21
- return "#{safe_subject}.html" unless Postmortem.config.timestamp
25
+ def index
26
+ @index ||= Index.new(index_path, path, @mail)
27
+ end
22
28
 
23
- "#{timestamp}__#{safe_subject}.html"
29
+ def identity
30
+ @identity ||= Identity.new
24
31
  end
25
32
 
26
- def timestamp
27
- Time.now.strftime('%Y-%m-%d_%H-%M-%S')
33
+ def subject
34
+ return 'no-subject' if @mail.subject.nil? || @mail.subject.empty?
35
+
36
+ @mail.subject
28
37
  end
29
38
 
30
39
  def safe_subject
31
- return 'no-subject' if @adapter.subject.nil? || @adapter.subject.empty?
32
-
33
- @adapter.subject.tr(' ', '_').split('').select { |char| safe_chars.include?(char) }.join
40
+ subject.tr(' ', '_').split('').select { |char| safe_chars.include?(char) }.join
34
41
  end
35
42
 
36
43
  def safe_chars
@@ -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
@@ -0,0 +1,68 @@
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, mail)
7
+ @index_path = index_path
8
+ @mail_path = mail_path
9
+ @mail = mail
10
+ end
11
+
12
+ def content
13
+ mail_path = @mail_path
14
+ ERB.new(File.read(template_path), nil, '-').result(binding)
15
+ end
16
+
17
+ def size
18
+ encoded_index.size
19
+ end
20
+
21
+ private
22
+
23
+ def uuid
24
+ @uuid ||= SecureRandom.uuid
25
+ end
26
+
27
+ def timestamp
28
+ Time.now.iso8601
29
+ end
30
+
31
+ def encoded_index
32
+ return [encoded_mail] unless @index_path.file?
33
+
34
+ @encoded_index ||= [encoded_mail] + lines[index(:start)..index(:end)]
35
+ end
36
+
37
+ def encoded_mail
38
+ Base64.encode64(mail_data.to_json).split("\n").join
39
+ end
40
+
41
+ def mail_data
42
+ {
43
+ subject: @mail.subject || '(no subject)',
44
+ timestamp: timestamp,
45
+ path: @mail_path,
46
+ id: @mail.id,
47
+ content: @mail.serializable
48
+ }
49
+ end
50
+
51
+ def lines
52
+ @lines ||= @index_path.read.split("\n")
53
+ end
54
+
55
+ def index(position)
56
+ offset = { start: 1, end: -1 }.fetch(position)
57
+ lines.index(marker(position)) + offset
58
+ end
59
+
60
+ def marker(position)
61
+ "### INDEX #{position.to_s.upcase}"
62
+ end
63
+
64
+ def template_path
65
+ File.expand_path(File.join(__dir__, '..', '..', 'layout', 'postmortem_index.html.erb'))
66
+ end
67
+ end
68
+ end