postmortem 0.2.0 → 0.2.6

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.
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
- 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 = [
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
- 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
- });
41
+ reloadIdentityIframeTimeout = setTimeout(() => identityIframe.src += '', 3000);
31
42
 
32
- setInterval(function () { indexIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
43
+ setInterval(function () { identityIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
33
44
 
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); };
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(true);
59
+ setColumnView(!POSTMORTEM.downloadedPreview);
47
60
  setHeadersView(true);
61
+ setEnabled(columnSwitch);
62
+ setOn(columnSwitch);
63
+ setHidden(inbox, POSTMORTEM.downloadedPreview);
48
64
 
49
- $('[data-toggle="tooltip"]').tooltip();
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
- 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');
83
+ $('[data-toggle="tooltip"]').tooltip();
84
+ }
59
85
 
60
- var setHeadersView = function (enableHeadersView) {
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
- var setColumnView = function (enableTwoColumnView) {
72
- var container = document.querySelector('.container');
73
- twoColumnView = enableTwoColumnView;
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
- var contexts = ['source', 'text', 'html'];
113
+ const contexts = ['source', 'text', 'html'];
86
114
 
87
- var views = {
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
- var toolbar = {
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
- var setOn = function(element) {
129
+ const setOn = function(element) {
101
130
  element.classList.add('text-primary');
102
131
  element.classList.remove('text-secondary');
103
132
  };
104
133
 
105
- var setOff = function(element) {
134
+ const setOff = function(element) {
106
135
  element.classList.add('text-secondary');
107
136
  element.classList.remove('text-primary');
108
137
  };
109
138
 
110
- var setDisabled = function(element) {
139
+ const setDisabled = function(element) {
111
140
  element.classList.add('disabled');
112
141
  element.classList.remove('text-secondary');
113
142
  };
114
143
 
115
- var setEnabled = function(element) {
144
+ const setEnabled = function(element) {
116
145
  element.classList.remove('disabled');
117
146
  element.classList.add('text-secondary');
118
147
  };
119
148
 
120
- var setVisible = function(element, visible) {
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
- var setView = function(context, ev) {
165
+ const setView = function(context, ev) {
129
166
  if (ev && $(ev.target).hasClass('disabled')) return;
130
- var key;
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
- setDisabled(toolbar.text);
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 renderInbox = function (mails) {
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 = `&mdash; ${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
- 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>`
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
- loadMail(mails[$target.data('email-index')].content);
219
- $('.inbox-item').removeClass('active');
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="<%= SecureRandom.uuid %>" id="index">
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) + "\n")
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 STDOUT if config.log_path.nil?
71
+ return $stdout if config.log_path.nil?
69
72
 
70
73
  @output_file ||= File.open(config.log_path, mode: 'a')
71
74
  end