postmortem 0.1.3 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +4 -46
- data/.ruby-version +1 -0
- data/Gemfile +0 -2
- data/README.md +20 -16
- data/doc/screenshot.png +0 -0
- data/layout/default.html.erb +68 -254
- data/layout/dependencies.css +11 -0
- data/layout/dependencies.js +14 -0
- data/layout/favicon.b64 +1 -0
- data/layout/headers_template.html +35 -0
- data/layout/layout.css +255 -0
- data/layout/layout.js +381 -0
- data/layout/postmortem_identity.html.erb +20 -0
- data/layout/postmortem_index.html.erb +25 -0
- data/lib/postmortem.rb +14 -2
- data/lib/postmortem/adapters.rb +1 -1
- data/lib/postmortem/adapters/action_mailer.rb +6 -41
- data/lib/postmortem/adapters/base.rb +60 -1
- data/lib/postmortem/adapters/mail.rb +3 -3
- data/lib/postmortem/adapters/pony.rb +3 -6
- data/lib/postmortem/configuration.rb +1 -5
- data/lib/postmortem/delivery.rb +20 -13
- data/lib/postmortem/identity.rb +20 -0
- data/lib/postmortem/index.rb +68 -0
- data/lib/postmortem/layout.rb +86 -3
- data/lib/postmortem/plugins/action_mailer.rb +7 -0
- data/lib/postmortem/version.rb +1 -1
- data/postmortem.gemspec +4 -2
- metadata +46 -6
@@ -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)
|
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
|
71
|
+
return $stdout if config.log_path.nil?
|
60
72
|
|
61
73
|
@output_file ||= File.open(config.log_path, mode: 'a')
|
62
74
|
end
|
data/lib/postmortem/adapters.rb
CHANGED
@@ -7,53 +7,18 @@ module Postmortem
|
|
7
7
|
private
|
8
8
|
|
9
9
|
def adapted
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
45
|
-
|
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
|
-
|
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,
|
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:
|
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, :
|
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
|
data/lib/postmortem/delivery.rb
CHANGED
@@ -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(
|
9
|
-
@
|
10
|
-
@path = Postmortem.config.preview_directory.join(
|
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
|
-
|
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
|
21
|
-
|
25
|
+
def index
|
26
|
+
@index ||= Index.new(index_path, path, @mail)
|
27
|
+
end
|
22
28
|
|
23
|
-
|
29
|
+
def identity
|
30
|
+
@identity ||= Identity.new
|
24
31
|
end
|
25
32
|
|
26
|
-
def
|
27
|
-
|
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
|
-
|
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
|