postmortem 0.2.0 → 0.2.6
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 -1
- data/Gemfile +0 -2
- data/Makefile +4 -0
- data/README.md +20 -16
- data/layout/default.html.erb +46 -26
- data/layout/dependencies.css +11 -0
- data/layout/dependencies.js +14 -0
- data/layout/favicon.b64 +1 -0
- data/layout/layout.css +124 -17
- data/layout/layout.js +212 -52
- data/layout/postmortem_identity.html.erb +20 -0
- data/layout/{index.html.erb → postmortem_index.html.erb} +3 -6
- data/lib/postmortem.rb +5 -2
- data/lib/postmortem/adapters.rb +1 -1
- data/lib/postmortem/adapters/action_mailer.rb +6 -41
- data/lib/postmortem/adapters/base.rb +43 -2
- data/lib/postmortem/adapters/mail.rb +3 -3
- data/lib/postmortem/adapters/pony.rb +3 -6
- data/lib/postmortem/configuration.rb +1 -9
- data/lib/postmortem/delivery.rb +9 -15
- data/lib/postmortem/identity.rb +20 -0
- data/lib/postmortem/index.rb +18 -6
- data/lib/postmortem/layout.rb +20 -1
- data/lib/postmortem/plugins/action_mailer.rb +7 -0
- data/lib/postmortem/version.rb +1 -1
- data/postmortem.gemspec +6 -3
- metadata +54 -8
- data/doc/screenshot.png +0 -0
data/layout/layout.js
CHANGED
@@ -1,13 +1,30 @@
|
|
1
1
|
(function () {
|
2
|
+
if (POSTMORTEM && POSTMORTEM.downloadedPreview) document.title = 'PostMortem';
|
3
|
+
|
2
4
|
let previousInbox;
|
3
5
|
let currentView;
|
6
|
+
let reloadIdentityIframeTimeout;
|
7
|
+
let identityUuid;
|
4
8
|
let indexUuid;
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
let twoColumnView;
|
10
|
+
let headersView;
|
11
|
+
let indexIframeTimeout;
|
12
|
+
|
13
|
+
const inboxContent = [];
|
14
|
+
const headers = document.querySelector('.headers');
|
15
|
+
const inbox = document.querySelector('#inbox-container');
|
16
|
+
const inboxInfo = document.querySelector('#inbox-info');
|
17
|
+
const columnSwitch = document.querySelector('.column-switch');
|
18
|
+
const headersViewSwitch = document.querySelector('.headers-view-switch');
|
19
|
+
const readAllButton = document.querySelector('.read-all-button');
|
20
|
+
const showHideReadButton = document.querySelector('.show-hide-read-button');
|
21
|
+
const showHideReadIcon = document.querySelector('.show-hide-read-icon');
|
22
|
+
const htmlIframeDocument = document.querySelector("#html-iframe").contentDocument;
|
23
|
+
const indexIframe = document.querySelector("#index-iframe");
|
24
|
+
const identityIframe = document.querySelector("#identity-iframe");
|
25
|
+
const textIframeDocument = document.querySelector("#text-iframe").contentDocument;
|
26
|
+
const sourceIframeDocument = document.querySelector("#source-iframe").contentDocument;
|
27
|
+
const sourceHighlightBundle = [
|
11
28
|
'<link rel="stylesheet"',
|
12
29
|
' href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/agate.min.css"',
|
13
30
|
' integrity="sha512-mMMMPADD4HAIogAWZbv+WjZTC0itafUZNI0jQm48PMBTApXt11omF5jhS7U1kp3R2Pr6oGJ+JwQKiUkUwCQaUQ=="',
|
@@ -18,46 +35,55 @@
|
|
18
35
|
'<script>hljs.initHighlightingOnLoad();</' + 'script>',
|
19
36
|
].join('\n');
|
20
37
|
|
38
|
+
const storage = window.localStorage;
|
39
|
+
|
21
40
|
const initialize = () => {
|
22
|
-
|
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
|
-
});
|
41
|
+
reloadIdentityIframeTimeout = setTimeout(() => identityIframe.src += '', 3000);
|
31
42
|
|
32
|
-
setInterval(function () {
|
43
|
+
setInterval(function () { identityIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
|
33
44
|
|
34
|
-
toolbar.html.onclick
|
35
|
-
toolbar.text.onclick
|
36
|
-
toolbar.source.onclick
|
37
|
-
toolbar.headers.onclick
|
38
|
-
columnSwitch.onclick
|
45
|
+
toolbar.html.onclick = (ev) => setView('html', ev);
|
46
|
+
toolbar.text.onclick = (ev) => setView('text', ev);
|
47
|
+
toolbar.source.onclick = (ev) => setView('source', ev);
|
48
|
+
toolbar.headers.onclick = () => setHeadersView(!headersView);
|
49
|
+
columnSwitch.onclick = () => setColumnView(!twoColumnView);
|
50
|
+
readAllButton.onclick = () => markAllAsRead();
|
51
|
+
showHideReadButton.onclick = () => toggleHideReadMessages();
|
39
52
|
|
40
|
-
if (hasHtml) {
|
53
|
+
if (POSTMORTEM.hasHtml) {
|
41
54
|
setView('html');
|
42
55
|
} else {
|
43
56
|
setView('text');
|
44
57
|
}
|
45
58
|
|
46
|
-
setColumnView(
|
59
|
+
setColumnView(!POSTMORTEM.downloadedPreview);
|
47
60
|
setHeadersView(true);
|
61
|
+
setEnabled(columnSwitch);
|
62
|
+
setOn(columnSwitch);
|
63
|
+
setHidden(inbox, POSTMORTEM.downloadedPreview);
|
48
64
|
|
49
|
-
|
50
|
-
|
65
|
+
if (POSTMORTEM.downloadedPreview) {
|
66
|
+
setHidden(toolbar.download, true);
|
67
|
+
setHidden(columnSwitch, true);
|
68
|
+
} else {
|
69
|
+
window.addEventListener('message', function (ev) {
|
70
|
+
switch (ev.data.type) {
|
71
|
+
case 'index':
|
72
|
+
loadInbox(ev.data.uuid, ev.data.mails);
|
73
|
+
break;
|
74
|
+
case 'identity':
|
75
|
+
compareIdentity(ev.data.uuid);
|
76
|
+
break;
|
77
|
+
};
|
78
|
+
});
|
79
|
+
}
|
51
80
|
|
81
|
+
loadMail(POSTMORTEM.initialData);
|
52
82
|
|
53
|
-
|
54
|
-
|
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');
|
83
|
+
$('[data-toggle="tooltip"]').tooltip();
|
84
|
+
}
|
59
85
|
|
60
|
-
|
86
|
+
const setHeadersView = (enableHeadersView) => {
|
61
87
|
headersView = enableHeadersView;
|
62
88
|
if (enableHeadersView) {
|
63
89
|
setOn(headersViewSwitch);
|
@@ -68,9 +94,11 @@
|
|
68
94
|
}
|
69
95
|
};
|
70
96
|
|
71
|
-
|
72
|
-
|
73
|
-
|
97
|
+
const setColumnView = (enableTwoColumnView) => {
|
98
|
+
if (!inbox) return;
|
99
|
+
|
100
|
+
const container = document.querySelector('.container');
|
101
|
+
twoColumnView = POSTMORTEM.downloadedPreview ? false : enableTwoColumnView;
|
74
102
|
if (twoColumnView) {
|
75
103
|
setVisible(inbox, true);
|
76
104
|
setOn(columnSwitch);
|
@@ -82,42 +110,51 @@
|
|
82
110
|
}
|
83
111
|
};
|
84
112
|
|
85
|
-
|
113
|
+
const contexts = ['source', 'text', 'html'];
|
86
114
|
|
87
|
-
|
115
|
+
const views = {
|
88
116
|
source: document.querySelector('.source-view'),
|
89
117
|
html: document.querySelector('.html-view'),
|
90
118
|
text: document.querySelector('.text-view'),
|
91
119
|
};
|
92
120
|
|
93
|
-
|
121
|
+
const toolbar = {
|
94
122
|
source: document.querySelector('.source-view-switch'),
|
95
123
|
html: document.querySelector('.html-view-switch'),
|
96
124
|
text: document.querySelector('.text-view-switch'),
|
97
125
|
headers: document.querySelector('.headers-view-switch'),
|
126
|
+
download: document.querySelector('#download-link'),
|
98
127
|
};
|
99
128
|
|
100
|
-
|
129
|
+
const setOn = function(element) {
|
101
130
|
element.classList.add('text-primary');
|
102
131
|
element.classList.remove('text-secondary');
|
103
132
|
};
|
104
133
|
|
105
|
-
|
134
|
+
const setOff = function(element) {
|
106
135
|
element.classList.add('text-secondary');
|
107
136
|
element.classList.remove('text-primary');
|
108
137
|
};
|
109
138
|
|
110
|
-
|
139
|
+
const setDisabled = function(element) {
|
111
140
|
element.classList.add('disabled');
|
112
141
|
element.classList.remove('text-secondary');
|
113
142
|
};
|
114
143
|
|
115
|
-
|
144
|
+
const setEnabled = function(element) {
|
116
145
|
element.classList.remove('disabled');
|
117
146
|
element.classList.add('text-secondary');
|
118
147
|
};
|
119
148
|
|
120
|
-
|
149
|
+
const setHidden = function(element, hidden) {
|
150
|
+
if (hidden) {
|
151
|
+
element.classList.add('hidden');
|
152
|
+
} else {
|
153
|
+
element.classList.remove('hidden');
|
154
|
+
}
|
155
|
+
};
|
156
|
+
|
157
|
+
const setVisible = function(element, visible) {
|
121
158
|
if (visible) {
|
122
159
|
element.classList.add('visible');
|
123
160
|
} else {
|
@@ -125,9 +162,9 @@
|
|
125
162
|
}
|
126
163
|
};
|
127
164
|
|
128
|
-
|
165
|
+
const setView = function(context, ev) {
|
129
166
|
if (ev && $(ev.target).hasClass('disabled')) return;
|
130
|
-
|
167
|
+
let key;
|
131
168
|
for (i = 0; i < contexts.length; i++) {
|
132
169
|
key = contexts[i];
|
133
170
|
if (key === context) {
|
@@ -170,7 +207,7 @@
|
|
170
207
|
setDisabled(toolbar.text);
|
171
208
|
setView('html');
|
172
209
|
} else {
|
173
|
-
|
210
|
+
setEnabled(toolbar.text);
|
174
211
|
}
|
175
212
|
|
176
213
|
if (!mail.htmlBody) {
|
@@ -181,10 +218,23 @@
|
|
181
218
|
setEnabled(toolbar.html);
|
182
219
|
setEnabled(toolbar.source);
|
183
220
|
}
|
221
|
+
|
184
222
|
setView(currentView);
|
185
223
|
};
|
186
224
|
|
187
225
|
const loadMail = (mail) => {
|
226
|
+
const initializeScript = document.querySelector("#initialize-script");
|
227
|
+
const initObject = {
|
228
|
+
initialData: mail,
|
229
|
+
hasHtml: !!mail.htmlBody,
|
230
|
+
hasText: !!mail.textBody,
|
231
|
+
downloadedPreview: true,
|
232
|
+
};
|
233
|
+
|
234
|
+
initializeScript.text = [
|
235
|
+
`const POSTMORTEM = ${JSON.stringify(initObject)};`
|
236
|
+
].join('\n\n');
|
237
|
+
|
188
238
|
htmlIframeDocument.open();
|
189
239
|
htmlIframeDocument.write(mail.htmlBody);
|
190
240
|
htmlIframeDocument.close();
|
@@ -200,25 +250,135 @@
|
|
200
250
|
|
201
251
|
loadHeaders(mail);
|
202
252
|
loadToolbar(mail);
|
253
|
+
loadDownloadLink(mail);
|
254
|
+
|
255
|
+
highlightMail(mail);
|
256
|
+
markAsRead(mail);
|
257
|
+
updateInboxInfo();
|
258
|
+
};
|
259
|
+
|
260
|
+
const markAsRead = (mail) => {
|
261
|
+
storage.setItem(mail.id, 'read');
|
262
|
+
$(`li[data-email-id="${mail.id}"]`).removeClass('unread');
|
263
|
+
$(readAllButton).blur();
|
264
|
+
};
|
265
|
+
|
266
|
+
const showReadMessages = (show) => {
|
267
|
+
if (show) {
|
268
|
+
inbox.classList.remove('hide-read');
|
269
|
+
} else {
|
270
|
+
inbox.classList.add('hide-read');
|
271
|
+
}
|
203
272
|
};
|
204
273
|
|
205
|
-
const
|
274
|
+
const toggleHideReadMessages = () => {
|
275
|
+
const $target = $(showHideReadButton);
|
276
|
+
const $icon = $(showHideReadIcon);
|
277
|
+
|
278
|
+
if ($target.data('state') === 'hide') {
|
279
|
+
$target.data('state', 'show');
|
280
|
+
$target.attr('data-original-title', 'Hide read messages');
|
281
|
+
$target.attr('title', 'Hide read messages');
|
282
|
+
$icon.removeClass('fa-eye-slash');
|
283
|
+
$icon.addClass('fa fa-eye text-primary');
|
284
|
+
showReadMessages(true);
|
285
|
+
} else {
|
286
|
+
$target.data('state', 'hide');
|
287
|
+
$target.attr('data-original-title', 'Show read messages');
|
288
|
+
$target.attr('title', 'Show read messages');
|
289
|
+
$icon.removeClass('fa-eye text-primary');
|
290
|
+
$icon.addClass('fa fa-eye-slash');
|
291
|
+
showReadMessages(false);
|
292
|
+
}
|
293
|
+
|
294
|
+
$target.blur();
|
295
|
+
};
|
296
|
+
|
297
|
+
const markAllAsRead = () => {
|
298
|
+
inboxContent.forEach(mail => markAsRead(mail));
|
299
|
+
updateInboxInfo();
|
300
|
+
};
|
301
|
+
|
302
|
+
const isNewMail = (mail) => {
|
303
|
+
if (!storage.getItem(mail.id)) return true;
|
304
|
+
|
305
|
+
return false;
|
306
|
+
};
|
307
|
+
|
308
|
+
const highlightMail = (mail) => {
|
309
|
+
window.location.hash = mail.id;
|
310
|
+
const $target = $(`li[data-email-id="${mail.id}"]`);
|
311
|
+
$('.inbox-item').removeClass('active');
|
312
|
+
$target.addClass('active');
|
313
|
+
};
|
314
|
+
|
315
|
+
const loadDownloadLink = (mail) => {
|
316
|
+
const html = document.documentElement.innerHTML;
|
317
|
+
const start = html.indexOf('<!--INBOX-START-->');
|
318
|
+
const end = html.indexOf('<!--INBOX-END-->') + '<!--INBOX-END-->'.length;
|
319
|
+
const modifiedHtml = [html.substring(0, start), html.substring(end + 1, html.length)].join('');
|
320
|
+
const blob = new Blob([modifiedHtml], { type: 'application/octet-stream' });
|
321
|
+
const uri = window.URL.createObjectURL(blob);
|
322
|
+
$("#download-link").attr('href', uri);
|
323
|
+
console.log(mail.subject.replace(/[0-9a-zA-Z_ -]/gi, ''));
|
324
|
+
$("#download-link").attr('download', mail.subject.replace(/[^0-9a-zA-Z_ -]/gi, '') + '.html');
|
325
|
+
};
|
326
|
+
|
327
|
+
const compareIdentity = (uuid) => {
|
328
|
+
clearTimeout(reloadIdentityIframeTimeout);
|
329
|
+
reloadIdentityIframeTimeout = setTimeout(() => identityIframe.src += '', 3000);
|
330
|
+
if (identityUuid !== uuid) {
|
331
|
+
indexIframe.src += '';
|
332
|
+
clearTimeout(indexIframeTimeout);
|
333
|
+
indexIframeTimeout = setInterval(function () { indexIframe.contentWindow.postMessage('HELO', '*'); }, 200);
|
334
|
+
}
|
335
|
+
identityUuid = uuid;
|
336
|
+
};
|
337
|
+
|
338
|
+
const updateInboxInfo = () => {
|
339
|
+
if (!inboxContent.length) return;
|
340
|
+
|
341
|
+
const unreadCount = inboxContent.filter((mail) => isNewMail(mail)).length;
|
342
|
+
document.title = `PostMortem ${unreadCount}/${inboxContent.length} (unread/total)`;
|
343
|
+
inboxInfo.textContent = `${inboxContent.length} emails (${unreadCount} unread)`;
|
344
|
+
inboxInfo.innerHTML = `— ${inboxInfo.innerHTML}`;
|
345
|
+
};
|
346
|
+
|
347
|
+
const loadInbox = (uuid, mails) => {
|
348
|
+
clearTimeout(indexIframeTimeout);
|
349
|
+
inboxContent.splice(0, Infinity, ...mails)
|
350
|
+
if (uuid === indexUuid) {
|
351
|
+
return;
|
352
|
+
}
|
353
|
+
const mailsById = {};
|
206
354
|
const html = mails.map((mail, index) => {
|
207
355
|
const parsedTimestamp = new Date(mail.timestamp);
|
208
356
|
const timestampSpan = `<span class="timestamp">${parsedTimestamp.toLocaleString()}</span>`;
|
209
357
|
const classes = ['list-group-item', 'inbox-item'];
|
210
|
-
|
211
|
-
|
358
|
+
|
359
|
+
if (window.location.hash === '#' + mail.id) classes.push('active');
|
360
|
+
if (isNewMail(mail)) classes.push('unread');
|
361
|
+
|
362
|
+
mailsById[mail.id] = mail;
|
363
|
+
|
364
|
+
return [`<li data-email-id="${mail.id}" class="${classes.join(' ')}">`,
|
365
|
+
`<a title="${mail.subject}" href="javascript:void(0)">`,
|
366
|
+
`<i class="fa fa-envelope-open read-icon"></i>`,
|
367
|
+
`<i class="fa fa-envelope unread-icon"></i>${mail.subject}`,
|
368
|
+
`</a>`,
|
369
|
+
`${timestampSpan}</li>`].join('');
|
212
370
|
});
|
371
|
+
updateInboxInfo();
|
213
372
|
if (arrayIdentical(html, previousInbox)) return;
|
214
373
|
previousInbox = html;
|
215
374
|
$('#inbox').html('<ul class="list-group">' + html.join('\n') + '</ul>');
|
216
375
|
$('.inbox-item').click((ev) => {
|
217
376
|
const $target = $(ev.currentTarget);
|
218
|
-
|
219
|
-
|
220
|
-
$target.addClass('active');
|
377
|
+
const id = $target.data('email-id');
|
378
|
+
setTimeout(() => loadMail(mailsById[id].content), 0);
|
221
379
|
});
|
380
|
+
|
381
|
+
indexUuid = uuid;
|
222
382
|
};
|
223
383
|
|
224
384
|
initialize();
|
@@ -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>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
2
|
<html>
|
3
3
|
<body>
|
4
|
-
<div data-uuid="<%=
|
4
|
+
<div data-uuid="<%= uuid %>" id="index">
|
5
5
|
### INDEX START
|
6
6
|
<% encoded_index.each do |encoded| -%>
|
7
7
|
<%= encoded %>
|
@@ -13,16 +13,13 @@
|
|
13
13
|
const filter = (line) => line !== '' && !line.startsWith('### INDEX');
|
14
14
|
const index = document.querySelector('#index');
|
15
15
|
const uuid = index.dataset.uuid;
|
16
|
+
const type = 'index';
|
16
17
|
const mails = index.innerText
|
17
18
|
.split('\n')
|
18
19
|
.filter(filter)
|
19
20
|
.map(line => JSON.parse(atob(line)));
|
20
|
-
ev.source.postMessage({ uuid, mails }, '*');
|
21
|
+
ev.source.postMessage({ uuid, type, mails }, '*');
|
21
22
|
});
|
22
|
-
|
23
|
-
setInterval(function () {
|
24
|
-
window.location.reload();
|
25
|
-
}, 1000);
|
26
23
|
</script>
|
27
24
|
</body>
|
28
25
|
</html>
|
data/lib/postmortem.rb
CHANGED
@@ -8,12 +8,15 @@ 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'
|
17
20
|
require 'postmortem/index'
|
18
21
|
|
19
22
|
# HTML email inspection tool.
|
@@ -54,7 +57,7 @@ module Postmortem
|
|
54
57
|
private
|
55
58
|
|
56
59
|
def log_delivery(delivery)
|
57
|
-
output_file.write(colorized(delivery.path.to_s)
|
60
|
+
output_file.write("#{colorized(delivery.path.to_s)}\n")
|
58
61
|
output_file.flush
|
59
62
|
end
|
60
63
|
|
@@ -65,7 +68,7 @@ module Postmortem
|
|
65
68
|
end
|
66
69
|
|
67
70
|
def output_file
|
68
|
-
return
|
71
|
+
return $stdout if config.log_path.nil?
|
69
72
|
|
70
73
|
@output_file ||= File.open(config.log_path, mode: 'a')
|
71
74
|
end
|