postmortem 0.1.3 → 0.2.5

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>
@@ -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