postmortem 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/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
- data/lib/postmortem.rb +9 -0
- 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_s).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
|
+
})();
|
@@ -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
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)
|
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
|