postmortem 0.1.2 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/README.md +2 -2
- data/layout/default.html.erb +32 -238
- data/layout/headers_template.html +35 -0
- data/layout/index.html.erb +28 -0
- data/layout/layout.css +148 -0
- data/layout/layout.js +225 -0
- data/lib/postmortem.rb +9 -0
- data/lib/postmortem/adapters/base.rb +19 -1
- data/lib/postmortem/configuration.rb +5 -1
- data/lib/postmortem/delivery.rb +23 -10
- data/lib/postmortem/index.rb +56 -0
- data/lib/postmortem/layout.rb +67 -3
- data/lib/postmortem/version.rb +1 -1
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 530858ff7f742c65d8c002f5044caaf306efd7629e1494b27a70d7cf6da19b89
|
4
|
+
data.tar.gz: af36040935d5ea5c012ea293ee9aa459b4b3d89b51e033d02741774be9d4eb6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b21c568f8b4a0668a97d43b296d26b86434c5aabb33af5327d0ab4c59b5375f81f632ff29e8eab5c2fb05a8db591392f97fe709fa980c266660a99a4cdcd0ec4
|
7
|
+
data.tar.gz: def87f9d2630d6dfd6bdd8315e6e1b3865f81edf4e8918e24a4ebcd5669cf96dbd0f606f5c9d30249787d32be51e62318790dede38f14d850a074eb47fef0aa5
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5.3
|
data/README.md
CHANGED
@@ -23,7 +23,7 @@ Add the gem to your application's Gemfile:
|
|
23
23
|
|
24
24
|
```ruby
|
25
25
|
group :development, :test do
|
26
|
-
gem 'postmortem', '~> 0.
|
26
|
+
gem 'postmortem', '~> 0.2.0'
|
27
27
|
end
|
28
28
|
```
|
29
29
|
|
@@ -68,7 +68,7 @@ Postmortem.configure do |config|
|
|
68
68
|
|
69
69
|
# Prefix all preview filenames with a timestamp (default: true).
|
70
70
|
# Setting to false allows refreshing the same path in your browser to view the latest version.
|
71
|
-
config.
|
71
|
+
config.timestamp = true
|
72
72
|
|
73
73
|
# Path to the Postmortem log file, where preview paths are written (default: STDOUT).
|
74
74
|
config.log_path = '/path/to/postmortem.log'
|
data/layout/default.html.erb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
<!DOCTYPE html>
|
1
2
|
<html>
|
2
3
|
<head>
|
3
4
|
<link rel="stylesheet"
|
@@ -11,154 +12,58 @@
|
|
11
12
|
integrity="sha512-zqEpZCxg7IVhGXy6EwdTb26cHl8IZzN/29Mj2/oSzkCKiLxHTi491mcC/K1NhShMWXu+WvfU5Z261XPc60lw7g=="
|
12
13
|
crossorigin="anonymous" />
|
13
14
|
<style>
|
14
|
-
|
15
|
-
height: 100%;
|
16
|
-
max-width: 35rem;
|
17
|
-
display: none;
|
18
|
-
}
|
19
|
-
|
20
|
-
.content {
|
21
|
-
background-color: #efefef;
|
22
|
-
padding: 1rem 2rem;
|
23
|
-
height: 100%;
|
24
|
-
}
|
25
|
-
|
26
|
-
.headers table {
|
27
|
-
background-color: #fff;
|
28
|
-
}
|
29
|
-
|
30
|
-
.preview {
|
31
|
-
display: none;
|
32
|
-
background-color: #fff;
|
33
|
-
padding: 2rem;
|
34
|
-
height: 100%;
|
35
|
-
}
|
36
|
-
|
37
|
-
.preview.source-view {
|
38
|
-
padding: 1.5rem;
|
39
|
-
}
|
40
|
-
|
41
|
-
.preview iframe {
|
42
|
-
border-style: none;
|
43
|
-
width: 100%;
|
44
|
-
height: 100%;
|
45
|
-
}
|
46
|
-
|
47
|
-
.main-row {
|
48
|
-
height: 100%;
|
49
|
-
}
|
50
|
-
|
51
|
-
.container {
|
52
|
-
height: 90vh;
|
53
|
-
margin-top: 2.8rem;
|
54
|
-
}
|
55
|
-
|
56
|
-
.container.full-width {
|
57
|
-
max-width: 100%;
|
58
|
-
}
|
59
|
-
|
60
|
-
.visible {
|
61
|
-
display: block;
|
62
|
-
}
|
63
|
-
|
64
|
-
.toolbar {
|
65
|
-
background-color: #fff;
|
66
|
-
text-align: right;
|
67
|
-
padding: 0.5rem 1rem;
|
68
|
-
position: fixed;
|
69
|
-
width: 100%;
|
70
|
-
}
|
71
|
-
|
72
|
-
.toolbar i {
|
73
|
-
font-size: 1.2rem;
|
74
|
-
cursor: pointer;
|
75
|
-
margin: 0 0.2rem;
|
76
|
-
}
|
77
|
-
|
78
|
-
.toolbar i.disabled {
|
79
|
-
cursor: default;
|
80
|
-
color: #ddd !important;
|
81
|
-
}
|
82
|
-
|
83
|
-
.toolbar .separator {
|
84
|
-
border-right: 1px solid #ccc;
|
85
|
-
margin-right: 0.5rem;
|
86
|
-
margin-left: 0.2rem;
|
87
|
-
}
|
15
|
+
<%= styles %>
|
88
16
|
</style>
|
89
17
|
</head>
|
90
18
|
<body>
|
91
19
|
<div class="toolbar">
|
92
20
|
|
93
21
|
<i data-toggle="tooltip"
|
94
|
-
title="
|
95
|
-
class="fa fa-code
|
22
|
+
title="View HTML Source"
|
23
|
+
class="fa fa-file-code-o source-view-switch"></i>
|
96
24
|
|
97
25
|
<i data-toggle="tooltip"
|
98
|
-
title="
|
99
|
-
class="fa fa-
|
26
|
+
title="View HTML Part"
|
27
|
+
class="fa fa-code html-view-switch"></i>
|
100
28
|
|
101
29
|
<i data-toggle="tooltip"
|
102
|
-
title="
|
30
|
+
title="View Plaintext Part"
|
103
31
|
class="fa fa-file-text-o text-view-switch"></i>
|
104
32
|
|
105
33
|
<span class="separator"></span>
|
106
34
|
|
107
35
|
<i data-toggle="tooltip"
|
108
|
-
title="Toggle
|
36
|
+
title="Toggle Inbox"
|
109
37
|
class="fa fa-columns column-switch"></i>
|
110
38
|
|
39
|
+
<i data-toggle="tooltip"
|
40
|
+
title="Toggle Headers"
|
41
|
+
class="fa fa-envelope-open-o headers-view-switch"></i>
|
42
|
+
|
111
43
|
</div>
|
112
44
|
<div class="content">
|
113
45
|
<div class="container full-width">
|
114
46
|
<div class="row main-row">
|
115
|
-
<div class="col
|
116
|
-
<table class="table table-hover table table-bordered">
|
117
|
-
<tbody>
|
118
|
-
<tr>
|
119
|
-
<th>Subject:</th>
|
120
|
-
<td><%= mail.subject %></td>
|
121
|
-
</tr>
|
122
|
-
|
123
|
-
<tr>
|
124
|
-
<th style="width: 7rem;">From:</th>
|
125
|
-
<td><%= format_email_array(mail.from) %></td>
|
126
|
-
</tr>
|
127
|
-
|
128
|
-
<tr>
|
129
|
-
<th>Reply-To:</th>
|
130
|
-
<td><%= format_email_array(mail.reply_to) %></td>
|
131
|
-
</tr>
|
132
|
-
|
133
|
-
<tr>
|
134
|
-
<th>To:</th>
|
135
|
-
<td><%= format_email_array(mail.to) %></td>
|
136
|
-
</tr>
|
137
|
-
|
138
|
-
<tr>
|
139
|
-
<th>Cc:</th>
|
140
|
-
<td><%= format_email_array(mail.cc) %></td>
|
141
|
-
</tr>
|
142
|
-
|
143
|
-
<tr>
|
144
|
-
<th>Bcc:</th>
|
145
|
-
<td><%= format_email_array(mail.bcc) %></td>
|
146
|
-
</tr>
|
147
|
-
</tbody>
|
148
|
-
</table>
|
149
|
-
</div>
|
150
|
-
<div class="preview col html-view">
|
151
|
-
<iframe id="html-iframe"></iframe>
|
152
|
-
</div>
|
153
|
-
<div class="preview col text-view">
|
154
|
-
<iframe id="text-iframe"></iframe>
|
47
|
+
<div id="inbox" class="col inbox">
|
155
48
|
</div>
|
156
|
-
|
157
|
-
|
49
|
+
|
50
|
+
<div class="col right-column">
|
51
|
+
<div id="headers" class="row headers visible"></div>
|
52
|
+
|
53
|
+
<div class="preview row html-view">
|
54
|
+
<iframe id="html-iframe"></iframe>
|
55
|
+
</div>
|
56
|
+
<div class="preview row text-view">
|
57
|
+
<iframe id="text-iframe"></iframe>
|
58
|
+
</div>
|
59
|
+
<div class="preview row source-view">
|
60
|
+
<iframe id="source-iframe"></iframe>
|
61
|
+
</div>
|
158
62
|
</div>
|
159
63
|
</div>
|
160
64
|
</div>
|
161
65
|
</div>
|
66
|
+
<iframe id="index-iframe" src="index.html"></iframe>
|
162
67
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
|
163
68
|
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
|
164
69
|
crossorigin="anonymous"></script>
|
@@ -168,123 +73,12 @@
|
|
168
73
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"
|
169
74
|
integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV"
|
170
75
|
crossorigin="anonymous"></script>
|
76
|
+
<%= headers_template %>
|
171
77
|
<script>
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
var textIframeDocument = document.querySelector("#text-iframe").contentDocument;
|
178
|
-
textIframeDocument.write('<pre>' + <%= ERB::Util.html_escape(mail.text_body).to_json %> + '</pre>');
|
179
|
-
textIframeDocument.close();
|
180
|
-
|
181
|
-
var sourceHighlightBundle = [
|
182
|
-
'<link rel="stylesheet"',
|
183
|
-
' href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/agate.min.css"',
|
184
|
-
' integrity="sha512-mMMMPADD4HAIogAWZbv+WjZTC0itafUZNI0jQm48PMBTApXt11omF5jhS7U1kp3R2Pr6oGJ+JwQKiUkUwCQaUQ=="',
|
185
|
-
' crossorigin="anonymous" />',
|
186
|
-
'<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/highlight.min.js"',
|
187
|
-
' integrity="sha512-tHQeqtcNWlZtEh8As/4MmZ5qpy0wj04svWFK7MIzLmUVIzaHXS8eod9OmHxyBL1UET5Rchvw7Ih4ZDv5JojZww=="',
|
188
|
-
' crossorigin="anonymous"></' + 'script>',
|
189
|
-
'<script>hljs.initHighlightingOnLoad();</' + 'script>',
|
190
|
-
].join('\n');
|
191
|
-
|
192
|
-
var sourceIframeDocument = document.querySelector("#source-iframe").contentDocument;
|
193
|
-
sourceIframeDocument.write('<pre><code style="padding: 1rem;" class="language-html">' + <%= ERB::Util.html_escape(mail.html_body).to_json %> + '</code></pre>');
|
194
|
-
sourceIframeDocument.write(sourceHighlightBundle);
|
195
|
-
sourceIframeDocument.close();
|
196
|
-
|
197
|
-
var twoColumnView;
|
198
|
-
var hasHtml = <%= (!mail.html_body.nil?).to_json %>;
|
199
|
-
var hasText = <%= (!mail.text_body.nil?).to_json %>;
|
200
|
-
var headers = document.querySelector('.headers');
|
201
|
-
var columnSwitch = document.querySelector('.column-switch');
|
202
|
-
|
203
|
-
var setColumnView = function (enableTwoColumnView) {
|
204
|
-
var container = document.querySelector('.container');
|
205
|
-
twoColumnView = enableTwoColumnView;
|
206
|
-
if (twoColumnView) {
|
207
|
-
setVisible(headers, true);
|
208
|
-
setOn(columnSwitch);
|
209
|
-
container.classList.add('full-width');
|
210
|
-
} else {
|
211
|
-
setVisible(headers, false);
|
212
|
-
setOff(columnSwitch);
|
213
|
-
container.classList.remove('full-width');
|
214
|
-
}
|
215
|
-
};
|
216
|
-
|
217
|
-
var contexts = ['source', 'text', 'html'];
|
218
|
-
|
219
|
-
var views = {
|
220
|
-
source: document.querySelector('.source-view'),
|
221
|
-
html: document.querySelector('.html-view'),
|
222
|
-
text: document.querySelector('.text-view'),
|
223
|
-
};
|
224
|
-
|
225
|
-
var toolbar = {
|
226
|
-
source: document.querySelector('.source-view-switch'),
|
227
|
-
html: document.querySelector('.html-view-switch'),
|
228
|
-
text: document.querySelector('.text-view-switch'),
|
229
|
-
};
|
230
|
-
|
231
|
-
var setOn = function(element) {
|
232
|
-
element.classList.add('text-primary');
|
233
|
-
element.classList.remove('text-secondary');
|
234
|
-
};
|
235
|
-
|
236
|
-
var setOff = function(element) {
|
237
|
-
element.classList.add('text-secondary');
|
238
|
-
element.classList.remove('text-primary');
|
239
|
-
};
|
240
|
-
|
241
|
-
var setDisabled = function(element) {
|
242
|
-
element.classList.add('disabled');
|
243
|
-
element.classList.remove('text-secondary');
|
244
|
-
element.onclick = function () {};
|
245
|
-
};
|
246
|
-
|
247
|
-
var setVisible = function(element, visible) {
|
248
|
-
if (visible) {
|
249
|
-
element.classList.add('visible');
|
250
|
-
} else {
|
251
|
-
element.classList.remove('visible');
|
252
|
-
}
|
253
|
-
};
|
254
|
-
|
255
|
-
var setView = function(context) {
|
256
|
-
var key;
|
257
|
-
for (i = 0; i < contexts.length; i++) {
|
258
|
-
key = contexts[i];
|
259
|
-
if (key === context) {
|
260
|
-
setOn(toolbar[key]);
|
261
|
-
setVisible(views[key], true);
|
262
|
-
} else {
|
263
|
-
setOff(toolbar[key]);
|
264
|
-
setVisible(views[key], false);
|
265
|
-
}
|
266
|
-
}
|
267
|
-
};
|
268
|
-
|
269
|
-
toolbar.html.onclick = function () { setView('html'); };
|
270
|
-
toolbar.text.onclick = function () { setView('text'); };
|
271
|
-
toolbar.source.onclick = function () { setView('source'); };
|
272
|
-
columnSwitch.onclick = function () { setColumnView(!twoColumnView); };
|
273
|
-
|
274
|
-
if (!hasText) setDisabled(toolbar.text);
|
275
|
-
|
276
|
-
if (hasHtml) {
|
277
|
-
setView('html');
|
278
|
-
} else {
|
279
|
-
setDisabled(toolbar.html);
|
280
|
-
setDisabled(toolbar.source);
|
281
|
-
setView('text');
|
282
|
-
}
|
283
|
-
|
284
|
-
setColumnView(true);
|
285
|
-
|
286
|
-
$('[data-toggle="tooltip"]').tooltip();
|
287
|
-
})();
|
78
|
+
var initialData = <%= mail.serializable.to_json %>;
|
79
|
+
var hasHtml = <%= (!mail.html_body.nil?).to_json %>;
|
80
|
+
var hasText = <%= (!mail.text_body.nil?).to_json %>;
|
81
|
+
<%= javascript %>
|
288
82
|
</script>
|
289
83
|
</body>
|
290
84
|
</html>
|
@@ -0,0 +1,35 @@
|
|
1
|
+
<script id="headers-template" type="text/x-template">
|
2
|
+
<table class="table table-hover table table-bordered">
|
3
|
+
<tbody>
|
4
|
+
<tr>
|
5
|
+
<th>Subject:</th>
|
6
|
+
<td id="email-subject"></td>
|
7
|
+
</tr>
|
8
|
+
|
9
|
+
<tr>
|
10
|
+
<th style="width: 7rem;">From:</th>
|
11
|
+
<td id="email-from"></td>
|
12
|
+
</tr>
|
13
|
+
|
14
|
+
<tr>
|
15
|
+
<th>Reply-To:</th>
|
16
|
+
<td id="email-replyTo"></td>
|
17
|
+
</tr>
|
18
|
+
|
19
|
+
<tr>
|
20
|
+
<th>To:</th>
|
21
|
+
<td id="email-to"></td>
|
22
|
+
</tr>
|
23
|
+
|
24
|
+
<tr>
|
25
|
+
<th>Cc:</th>
|
26
|
+
<td id="email-cc"></td>
|
27
|
+
</tr>
|
28
|
+
|
29
|
+
<tr>
|
30
|
+
<th>Bcc:</th>
|
31
|
+
<td id="email-bcc"></td>
|
32
|
+
</tr>
|
33
|
+
</tbody>
|
34
|
+
</table>
|
35
|
+
</script>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<body>
|
4
|
+
<div data-uuid="<%= SecureRandom.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 mails = index.innerText
|
17
|
+
.split('\n')
|
18
|
+
.filter(filter)
|
19
|
+
.map(line => JSON.parse(atob(line)));
|
20
|
+
ev.source.postMessage({ uuid, mails }, '*');
|
21
|
+
});
|
22
|
+
|
23
|
+
setInterval(function () {
|
24
|
+
window.location.reload();
|
25
|
+
}, 1000);
|
26
|
+
</script>
|
27
|
+
</body>
|
28
|
+
</html>
|
data/layout/layout.css
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
.headers {
|
2
|
+
width: 100%;
|
3
|
+
display: none;
|
4
|
+
}
|
5
|
+
|
6
|
+
.row {
|
7
|
+
margin: 0;
|
8
|
+
}
|
9
|
+
|
10
|
+
#inbox {
|
11
|
+
overflow-y: auto;
|
12
|
+
height: 100%;
|
13
|
+
max-height: 100%;
|
14
|
+
display: none;
|
15
|
+
width: 35rem;
|
16
|
+
max-width: 35rem;
|
17
|
+
}
|
18
|
+
|
19
|
+
#inbox.visible {
|
20
|
+
display: block;
|
21
|
+
}
|
22
|
+
|
23
|
+
#inbox li span.timestamp {
|
24
|
+
font-size: 0.8rem;
|
25
|
+
margin-right: -0.5rem;
|
26
|
+
float: right;
|
27
|
+
color: #aaa;
|
28
|
+
position: absolute;
|
29
|
+
right: 1rem;
|
30
|
+
top: 0.8rem;
|
31
|
+
}
|
32
|
+
|
33
|
+
#inbox li a {
|
34
|
+
text-decoration: none;
|
35
|
+
background-color: transparent;
|
36
|
+
max-width: 22rem;
|
37
|
+
overflow: hidden;
|
38
|
+
display: block;
|
39
|
+
white-space: nowrap;
|
40
|
+
text-overflow: ellipsis
|
41
|
+
}
|
42
|
+
|
43
|
+
#inbox li a .unread-icon {
|
44
|
+
color: #007bff;
|
45
|
+
}
|
46
|
+
|
47
|
+
#inbox li a:visited .unread-icon {
|
48
|
+
color: #ffffff00;
|
49
|
+
}
|
50
|
+
|
51
|
+
#inbox li a:hover {
|
52
|
+
text-decoration: none;
|
53
|
+
}
|
54
|
+
|
55
|
+
#inbox li.active a {
|
56
|
+
color: #fff;
|
57
|
+
}
|
58
|
+
|
59
|
+
#inbox li.active span.timestamp {
|
60
|
+
color: #fff;
|
61
|
+
}
|
62
|
+
|
63
|
+
.content {
|
64
|
+
background-color: #efefef;
|
65
|
+
padding: 1rem 2rem;
|
66
|
+
height: 100%;
|
67
|
+
}
|
68
|
+
|
69
|
+
.headers table {
|
70
|
+
background-color: #fff;
|
71
|
+
}
|
72
|
+
|
73
|
+
.preview {
|
74
|
+
display: none;
|
75
|
+
background-color: #fff;
|
76
|
+
padding: 2rem;
|
77
|
+
height: 100%;
|
78
|
+
}
|
79
|
+
|
80
|
+
.preview.source-view {
|
81
|
+
padding: 1.5rem;
|
82
|
+
}
|
83
|
+
|
84
|
+
.preview iframe {
|
85
|
+
border-style: none;
|
86
|
+
width: 100%;
|
87
|
+
height: 100%;
|
88
|
+
}
|
89
|
+
|
90
|
+
.main-row {
|
91
|
+
height: 100%;
|
92
|
+
}
|
93
|
+
|
94
|
+
.container {
|
95
|
+
height: 90vh;
|
96
|
+
margin-top: 2.8rem;
|
97
|
+
}
|
98
|
+
|
99
|
+
.container.full-width {
|
100
|
+
max-width: 100%;
|
101
|
+
}
|
102
|
+
|
103
|
+
.visible {
|
104
|
+
display: block;
|
105
|
+
}
|
106
|
+
|
107
|
+
.toolbar {
|
108
|
+
background-color: #fff;
|
109
|
+
text-align: right;
|
110
|
+
padding: 0.5rem 1rem;
|
111
|
+
position: fixed;
|
112
|
+
width: 100%;
|
113
|
+
}
|
114
|
+
|
115
|
+
.toolbar i {
|
116
|
+
font-size: 1.2rem;
|
117
|
+
cursor: pointer;
|
118
|
+
margin: 0 0.2rem;
|
119
|
+
}
|
120
|
+
|
121
|
+
.toolbar i.disabled {
|
122
|
+
cursor: default;
|
123
|
+
color: #ddd !important;
|
124
|
+
}
|
125
|
+
|
126
|
+
.toolbar .separator {
|
127
|
+
border-right: 1px solid #ccc;
|
128
|
+
margin-right: 0.5rem;
|
129
|
+
margin-left: 0.2rem;
|
130
|
+
}
|
131
|
+
|
132
|
+
.right-column {
|
133
|
+
display: flex;
|
134
|
+
flex-flow: column;
|
135
|
+
height: 100%;
|
136
|
+
}
|
137
|
+
|
138
|
+
#index-iframe {
|
139
|
+
display: none;
|
140
|
+
}
|
141
|
+
|
142
|
+
.hidden {
|
143
|
+
display: none;
|
144
|
+
}
|
145
|
+
|
146
|
+
#toolbar-download:hover {
|
147
|
+
color: #007bff;
|
148
|
+
}
|
data/layout/layout.js
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
(function () {
|
2
|
+
let previousInbox;
|
3
|
+
let currentView;
|
4
|
+
let indexUuid;
|
5
|
+
|
6
|
+
var htmlIframeDocument = document.querySelector("#html-iframe").contentDocument;
|
7
|
+
var indexIframe = document.querySelector("#index-iframe");
|
8
|
+
var textIframeDocument = document.querySelector("#text-iframe").contentDocument;
|
9
|
+
var sourceIframeDocument = document.querySelector("#source-iframe").contentDocument;
|
10
|
+
var sourceHighlightBundle = [
|
11
|
+
'<link rel="stylesheet"',
|
12
|
+
' href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/agate.min.css"',
|
13
|
+
' integrity="sha512-mMMMPADD4HAIogAWZbv+WjZTC0itafUZNI0jQm48PMBTApXt11omF5jhS7U1kp3R2Pr6oGJ+JwQKiUkUwCQaUQ=="',
|
14
|
+
' crossorigin="anonymous" />',
|
15
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/highlight.min.js"',
|
16
|
+
' integrity="sha512-tHQeqtcNWlZtEh8As/4MmZ5qpy0wj04svWFK7MIzLmUVIzaHXS8eod9OmHxyBL1UET5Rchvw7Ih4ZDv5JojZww=="',
|
17
|
+
' crossorigin="anonymous"></' + 'script>',
|
18
|
+
'<script>hljs.initHighlightingOnLoad();</' + 'script>',
|
19
|
+
].join('\n');
|
20
|
+
|
21
|
+
const initialize = () => {
|
22
|
+
loadMail(initialData);
|
23
|
+
|
24
|
+
let reloadIframeTimeout = setTimeout(() => indexIframe.src += '', 3000);
|
25
|
+
window.addEventListener('message', function (ev) {
|
26
|
+
clearTimeout(reloadIframeTimeout);
|
27
|
+
reloadIframeTimeout = setTimeout(() => indexIframe.src += '', 3000);
|
28
|
+
if (indexUuid !== ev.data.uuid) renderInbox(ev.data.mails);
|
29
|
+
indexUuid = ev.data.uuid;
|
30
|
+
});
|
31
|
+
|
32
|
+
setInterval(function () { indexIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
|
33
|
+
|
34
|
+
toolbar.html.onclick = function (ev) { setView('html', ev); };
|
35
|
+
toolbar.text.onclick = function (ev) { setView('text', ev); };
|
36
|
+
toolbar.source.onclick = function (ev) { setView('source', ev); };
|
37
|
+
toolbar.headers.onclick = function () { setHeadersView(!headersView); };
|
38
|
+
columnSwitch.onclick = function () { setColumnView(!twoColumnView); };
|
39
|
+
|
40
|
+
if (hasHtml) {
|
41
|
+
setView('html');
|
42
|
+
} else {
|
43
|
+
setView('text');
|
44
|
+
}
|
45
|
+
|
46
|
+
setColumnView(true);
|
47
|
+
setHeadersView(true);
|
48
|
+
|
49
|
+
$('[data-toggle="tooltip"]').tooltip();
|
50
|
+
}
|
51
|
+
|
52
|
+
|
53
|
+
var twoColumnView;
|
54
|
+
var headersView;
|
55
|
+
var headers = document.querySelector('.headers');
|
56
|
+
var inbox = document.querySelector('#inbox');
|
57
|
+
var columnSwitch = document.querySelector('.column-switch');
|
58
|
+
var headersViewSwitch = document.querySelector('.headers-view-switch');
|
59
|
+
|
60
|
+
var setHeadersView = function (enableHeadersView) {
|
61
|
+
headersView = enableHeadersView;
|
62
|
+
if (enableHeadersView) {
|
63
|
+
setOn(headersViewSwitch);
|
64
|
+
headers.classList.add('visible');
|
65
|
+
} else {
|
66
|
+
setOff(headersViewSwitch);
|
67
|
+
headers.classList.remove('visible');
|
68
|
+
}
|
69
|
+
};
|
70
|
+
|
71
|
+
var setColumnView = function (enableTwoColumnView) {
|
72
|
+
var container = document.querySelector('.container');
|
73
|
+
twoColumnView = enableTwoColumnView;
|
74
|
+
if (twoColumnView) {
|
75
|
+
setVisible(inbox, true);
|
76
|
+
setOn(columnSwitch);
|
77
|
+
container.classList.add('full-width');
|
78
|
+
} else {
|
79
|
+
setVisible(inbox, false);
|
80
|
+
setOff(columnSwitch);
|
81
|
+
container.classList.remove('full-width');
|
82
|
+
}
|
83
|
+
};
|
84
|
+
|
85
|
+
var contexts = ['source', 'text', 'html'];
|
86
|
+
|
87
|
+
var views = {
|
88
|
+
source: document.querySelector('.source-view'),
|
89
|
+
html: document.querySelector('.html-view'),
|
90
|
+
text: document.querySelector('.text-view'),
|
91
|
+
};
|
92
|
+
|
93
|
+
var toolbar = {
|
94
|
+
source: document.querySelector('.source-view-switch'),
|
95
|
+
html: document.querySelector('.html-view-switch'),
|
96
|
+
text: document.querySelector('.text-view-switch'),
|
97
|
+
headers: document.querySelector('.headers-view-switch'),
|
98
|
+
};
|
99
|
+
|
100
|
+
var setOn = function(element) {
|
101
|
+
element.classList.add('text-primary');
|
102
|
+
element.classList.remove('text-secondary');
|
103
|
+
};
|
104
|
+
|
105
|
+
var setOff = function(element) {
|
106
|
+
element.classList.add('text-secondary');
|
107
|
+
element.classList.remove('text-primary');
|
108
|
+
};
|
109
|
+
|
110
|
+
var setDisabled = function(element) {
|
111
|
+
element.classList.add('disabled');
|
112
|
+
element.classList.remove('text-secondary');
|
113
|
+
};
|
114
|
+
|
115
|
+
var setEnabled = function(element) {
|
116
|
+
element.classList.remove('disabled');
|
117
|
+
element.classList.add('text-secondary');
|
118
|
+
};
|
119
|
+
|
120
|
+
var setVisible = function(element, visible) {
|
121
|
+
if (visible) {
|
122
|
+
element.classList.add('visible');
|
123
|
+
} else {
|
124
|
+
element.classList.remove('visible');
|
125
|
+
}
|
126
|
+
};
|
127
|
+
|
128
|
+
var setView = function(context, ev) {
|
129
|
+
if (ev && $(ev.target).hasClass('disabled')) return;
|
130
|
+
var key;
|
131
|
+
for (i = 0; i < contexts.length; i++) {
|
132
|
+
key = contexts[i];
|
133
|
+
if (key === context) {
|
134
|
+
setOn(toolbar[key]);
|
135
|
+
setVisible(views[key], true);
|
136
|
+
} else {
|
137
|
+
setOff(toolbar[key]);
|
138
|
+
setVisible(views[key], false);
|
139
|
+
}
|
140
|
+
}
|
141
|
+
currentView = context;
|
142
|
+
};
|
143
|
+
|
144
|
+
const arrayIdentical = (a, b) => {
|
145
|
+
if (a && !b) return false;
|
146
|
+
if (a.length !== b.length) return false;
|
147
|
+
if (!a.every((item, index) => item === b[index])) return false;
|
148
|
+
|
149
|
+
return true;
|
150
|
+
};
|
151
|
+
|
152
|
+
const htmlEscape = (html) => {
|
153
|
+
return $("<div></div>").text(html).html();
|
154
|
+
};
|
155
|
+
|
156
|
+
const $headersTemplate = $("#headers-template");
|
157
|
+
|
158
|
+
const loadHeaders = (mail) => {
|
159
|
+
$template = $headersTemplate.clone();
|
160
|
+
$('#headers').html($template.html());
|
161
|
+
['subject', 'from', 'replyTo', 'to', 'cc', 'bcc'].forEach(item => {
|
162
|
+
const $item = $(`#email-${item}`)
|
163
|
+
$item.text(mail[item]);
|
164
|
+
if (!mail[item]) $item.parent().addClass('hidden');
|
165
|
+
});
|
166
|
+
};
|
167
|
+
|
168
|
+
const loadToolbar = (mail) => {
|
169
|
+
if (!mail.textBody) {
|
170
|
+
setDisabled(toolbar.text);
|
171
|
+
setView('html');
|
172
|
+
} else {
|
173
|
+
setDisabled(toolbar.text);
|
174
|
+
}
|
175
|
+
|
176
|
+
if (!mail.htmlBody) {
|
177
|
+
setDisabled(toolbar.html);
|
178
|
+
setDisabled(toolbar.source);
|
179
|
+
setView('text');
|
180
|
+
} else {
|
181
|
+
setEnabled(toolbar.html);
|
182
|
+
setEnabled(toolbar.source);
|
183
|
+
}
|
184
|
+
setView(currentView);
|
185
|
+
};
|
186
|
+
|
187
|
+
const loadMail = (mail) => {
|
188
|
+
htmlIframeDocument.open();
|
189
|
+
htmlIframeDocument.write(mail.htmlBody);
|
190
|
+
htmlIframeDocument.close();
|
191
|
+
|
192
|
+
textIframeDocument.open();
|
193
|
+
textIframeDocument.write(`<pre>${htmlEscape(mail.textBody)}</pre>`);
|
194
|
+
textIframeDocument.close();
|
195
|
+
|
196
|
+
sourceIframeDocument.open();
|
197
|
+
sourceIframeDocument.write(`<pre><code style="padding: 1rem;" class="language-html">${htmlEscape(mail.htmlBody)}</code></pre>`);
|
198
|
+
sourceIframeDocument.write(sourceHighlightBundle);
|
199
|
+
sourceIframeDocument.close();
|
200
|
+
|
201
|
+
loadHeaders(mail);
|
202
|
+
loadToolbar(mail);
|
203
|
+
};
|
204
|
+
|
205
|
+
const renderInbox = function (mails) {
|
206
|
+
const html = mails.map((mail, index) => {
|
207
|
+
const parsedTimestamp = new Date(mail.timestamp);
|
208
|
+
const timestampSpan = `<span class="timestamp">${parsedTimestamp.toLocaleString()}</span>`;
|
209
|
+
const classes = ['list-group-item', 'inbox-item'];
|
210
|
+
if (window.location.href.split('#')[0].endsWith(mail.path)) classes.push('active');
|
211
|
+
return `<li data-email-index="${index}" class="${classes.join(' ')}"><a title="${mail.subject}" href="javascript:void(0)">${mail.subject}</a>${timestampSpan}</li>`
|
212
|
+
});
|
213
|
+
if (arrayIdentical(html, previousInbox)) return;
|
214
|
+
previousInbox = html;
|
215
|
+
$('#inbox').html('<ul class="list-group">' + html.join('\n') + '</ul>');
|
216
|
+
$('.inbox-item').click((ev) => {
|
217
|
+
const $target = $(ev.currentTarget);
|
218
|
+
loadMail(mails[$target.data('email-index')].content);
|
219
|
+
$('.inbox-item').removeClass('active');
|
220
|
+
$target.addClass('active');
|
221
|
+
});
|
222
|
+
};
|
223
|
+
|
224
|
+
initialize();
|
225
|
+
})();
|
data/lib/postmortem.rb
CHANGED
@@ -14,6 +14,7 @@ require 'postmortem/adapters'
|
|
14
14
|
require 'postmortem/delivery'
|
15
15
|
require 'postmortem/layout'
|
16
16
|
require 'postmortem/configuration'
|
17
|
+
require 'postmortem/index'
|
17
18
|
|
18
19
|
# HTML email inspection tool.
|
19
20
|
module Postmortem
|
@@ -22,6 +23,10 @@ module Postmortem
|
|
22
23
|
class << self
|
23
24
|
attr_reader :config
|
24
25
|
|
26
|
+
def root
|
27
|
+
Pathname.new(__dir__).parent
|
28
|
+
end
|
29
|
+
|
25
30
|
def record_delivery(mail)
|
26
31
|
Delivery.new(mail)
|
27
32
|
.tap(&:record)
|
@@ -42,6 +47,10 @@ module Postmortem
|
|
42
47
|
yield @config if block_given?
|
43
48
|
end
|
44
49
|
|
50
|
+
def clear_inbox
|
51
|
+
config.preview_directory.rmtree
|
52
|
+
end
|
53
|
+
|
45
54
|
private
|
46
55
|
|
47
56
|
def log_delivery(delivery)
|
@@ -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].freeze
|
6
|
+
|
5
7
|
# Base interface implementation for all Postmortem adapters.
|
6
8
|
class Base
|
7
9
|
def initialize(data)
|
@@ -9,7 +11,15 @@ 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
|
+
FIELDS.map { |field| [camelize(field.to_s), public_send(field)] }.to_h
|
20
|
+
end
|
21
|
+
|
22
|
+
FIELDS.each do |method_name|
|
13
23
|
define_method method_name do
|
14
24
|
@adapted[method_name]
|
15
25
|
end
|
@@ -20,6 +30,14 @@ module Postmortem
|
|
20
30
|
def adapted
|
21
31
|
raise NotImplementedError, 'Adapter child class must implement #adapted'
|
22
32
|
end
|
33
|
+
|
34
|
+
def camelize(string)
|
35
|
+
string
|
36
|
+
.split('_')
|
37
|
+
.each_with_index
|
38
|
+
.map { |substring, index| index.zero? ? substring : substring.capitalize }
|
39
|
+
.join
|
40
|
+
end
|
23
41
|
end
|
24
42
|
end
|
25
43
|
end
|
@@ -3,13 +3,17 @@
|
|
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, :timestamp, :mail_skip_delivery, :token
|
7
7
|
attr_accessor :log_path
|
8
8
|
|
9
9
|
def timestamp
|
10
10
|
defined?(@timestamp) ? @timestamp : true
|
11
11
|
end
|
12
12
|
|
13
|
+
def token
|
14
|
+
defined?(@token) ? @token : true
|
15
|
+
end
|
16
|
+
|
13
17
|
def colorize
|
14
18
|
defined?(@colorize) ? @colorize : true
|
15
19
|
end
|
data/lib/postmortem/delivery.rb
CHANGED
@@ -3,34 +3,47 @@
|
|
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
|
-
@
|
8
|
+
def initialize(mail)
|
9
|
+
@mail = mail
|
10
10
|
@path = Postmortem.config.preview_directory.join(filename)
|
11
|
+
@index_path = Postmortem.config.preview_directory.join('index.html')
|
11
12
|
end
|
12
13
|
|
13
14
|
def record
|
14
15
|
path.parent.mkpath
|
15
|
-
|
16
|
+
content = Layout.new(@mail).content
|
17
|
+
path.write(content)
|
18
|
+
index_path.write(Index.new(index_path, path, timestamp, @mail).content)
|
16
19
|
end
|
17
20
|
|
18
21
|
private
|
19
22
|
|
20
23
|
def filename
|
21
|
-
|
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
|
22
27
|
|
23
|
-
"#{
|
28
|
+
"#{timestamp_chunk}#{token_chunk}#{safe_subject}.html"
|
24
29
|
end
|
25
30
|
|
26
31
|
def timestamp
|
27
|
-
Time.now
|
32
|
+
@timestamp ||= Time.now
|
28
33
|
end
|
29
34
|
|
30
|
-
def
|
31
|
-
|
35
|
+
def token
|
36
|
+
SecureRandom.hex(4)
|
37
|
+
end
|
38
|
+
|
39
|
+
def subject
|
40
|
+
return 'no-subject' if @mail.subject.nil? || @mail.subject.empty?
|
32
41
|
|
33
|
-
@
|
42
|
+
@mail.subject
|
43
|
+
end
|
44
|
+
|
45
|
+
def safe_subject
|
46
|
+
subject.tr(' ', '_').split('').select { |char| safe_chars.include?(char) }.join
|
34
47
|
end
|
35
48
|
|
36
49
|
def safe_chars
|
@@ -0,0 +1,56 @@
|
|
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, timestamp, mail)
|
7
|
+
@index_path = index_path
|
8
|
+
@mail_path = mail_path
|
9
|
+
@timestamp = timestamp.iso8601
|
10
|
+
@mail = mail
|
11
|
+
end
|
12
|
+
|
13
|
+
def content
|
14
|
+
mail_path = @mail_path
|
15
|
+
ERB.new(File.read(template_path), nil, '-').result(binding)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def encoded_index
|
21
|
+
return [encoded_mail] unless @index_path.file?
|
22
|
+
|
23
|
+
[encoded_mail] + lines[index(:start)..index(:end)]
|
24
|
+
end
|
25
|
+
|
26
|
+
def encoded_mail
|
27
|
+
Base64.urlsafe_encode64(mail_data.to_json)
|
28
|
+
end
|
29
|
+
|
30
|
+
def mail_data
|
31
|
+
{
|
32
|
+
subject: @mail.subject || '(no subject)',
|
33
|
+
timestamp: @timestamp,
|
34
|
+
path: @mail_path,
|
35
|
+
content: @mail.serializable
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def lines
|
40
|
+
@lines ||= @index_path.read.split("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
def index(position)
|
44
|
+
offset = { start: 1, end: -1 }.fetch(position)
|
45
|
+
lines.index(marker(position)) + offset
|
46
|
+
end
|
47
|
+
|
48
|
+
def marker(position)
|
49
|
+
"### INDEX #{position.to_s.upcase}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def template_path
|
53
|
+
File.expand_path(File.join(__dir__, '..', '..', 'layout', 'index.html.erb'))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/postmortem/layout.rb
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
module Postmortem
|
4
4
|
# Wraps provided body in an enclosing layout for presentation purposes.
|
5
5
|
class Layout
|
6
|
-
def initialize(
|
7
|
-
@
|
6
|
+
def initialize(mail)
|
7
|
+
@mail = mail
|
8
8
|
end
|
9
9
|
|
10
10
|
def format_email_array(array)
|
@@ -12,8 +12,72 @@ module Postmortem
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def content
|
15
|
-
mail = @
|
15
|
+
mail = @mail
|
16
|
+
mail.html_body = with_inlined_images(mail.html_body) if defined?(Nokogiri)
|
16
17
|
ERB.new(Postmortem.config.layout.read).result(binding)
|
17
18
|
end
|
19
|
+
|
20
|
+
def styles
|
21
|
+
default_layout_directory.join('layout.css').read
|
22
|
+
end
|
23
|
+
|
24
|
+
def javascript
|
25
|
+
default_layout_directory.join('layout.js').read
|
26
|
+
end
|
27
|
+
|
28
|
+
def headers_template
|
29
|
+
default_layout_directory.join('headers_template.html').read
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def default_layout_directory
|
35
|
+
Postmortem.root.join('layout')
|
36
|
+
end
|
37
|
+
|
38
|
+
def with_inlined_images(body)
|
39
|
+
parsed = Nokogiri::HTML.parse(body)
|
40
|
+
parsed.css('img').each do |img|
|
41
|
+
uri = URI(img['src'])
|
42
|
+
next unless local_file?(uri)
|
43
|
+
|
44
|
+
path = located_image(uri)
|
45
|
+
img['src'] = encoded_image(path) unless path.nil?
|
46
|
+
end
|
47
|
+
parsed.to_s
|
48
|
+
end
|
49
|
+
|
50
|
+
def local_file?(uri)
|
51
|
+
return true if uri.host.nil?
|
52
|
+
return true if /^www\.example\.[a-z]+$/.match(uri.host)
|
53
|
+
return true if %w[127.0.0.1 localhost].include?(uri.host)
|
54
|
+
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def located_image(uri)
|
59
|
+
path = uri.path.partition('/').last
|
60
|
+
common_locations.each do |location|
|
61
|
+
full_path = location.join(path)
|
62
|
+
next unless full_path.file?
|
63
|
+
|
64
|
+
return full_path
|
65
|
+
end
|
66
|
+
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def encoded_image(path)
|
71
|
+
"data:#{mime_type(path)};base64,#{Base64.encode64(path.read)}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def common_locations
|
75
|
+
['public/assets', 'app/assets/images'].map { |path| Pathname.new(path) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def mime_type(path)
|
79
|
+
extension = path.extname.partition('.').last
|
80
|
+
extension == 'jpg' ? 'jpeg' : extension
|
81
|
+
end
|
18
82
|
end
|
19
83
|
end
|
data/lib/postmortem/version.rb
CHANGED
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.
|
4
|
+
version: 0.2.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-
|
11
|
+
date: 2021-01-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mail
|
@@ -132,6 +132,7 @@ files:
|
|
132
132
|
- ".gitignore"
|
133
133
|
- ".rspec"
|
134
134
|
- ".rubocop.yml"
|
135
|
+
- ".ruby-version"
|
135
136
|
- Gemfile
|
136
137
|
- LICENSE.txt
|
137
138
|
- Makefile
|
@@ -141,6 +142,10 @@ files:
|
|
141
142
|
- bin/setup
|
142
143
|
- doc/screenshot.png
|
143
144
|
- layout/default.html.erb
|
145
|
+
- layout/headers_template.html
|
146
|
+
- layout/index.html.erb
|
147
|
+
- layout/layout.css
|
148
|
+
- layout/layout.js
|
144
149
|
- lib/postmortem.rb
|
145
150
|
- lib/postmortem/adapters.rb
|
146
151
|
- lib/postmortem/adapters/action_mailer.rb
|
@@ -149,6 +154,7 @@ files:
|
|
149
154
|
- lib/postmortem/adapters/pony.rb
|
150
155
|
- lib/postmortem/configuration.rb
|
151
156
|
- lib/postmortem/delivery.rb
|
157
|
+
- lib/postmortem/index.rb
|
152
158
|
- lib/postmortem/layout.rb
|
153
159
|
- lib/postmortem/plugins/action_mailer.rb
|
154
160
|
- lib/postmortem/plugins/mail.rb
|
@@ -177,7 +183,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
177
183
|
- !ruby/object:Gem::Version
|
178
184
|
version: '0'
|
179
185
|
requirements: []
|
180
|
-
|
186
|
+
rubyforge_project:
|
187
|
+
rubygems_version: 2.7.6
|
181
188
|
signing_key:
|
182
189
|
specification_version: 4
|
183
190
|
summary: Development HTML Email Inspection Tool
|