postmortem 0.2.0 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
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