postmortem 0.1.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/layout/layout.css ADDED
@@ -0,0 +1,160 @@
1
+ .headers {
2
+ width: 100%;
3
+ display: none;
4
+ }
5
+
6
+ .row {
7
+ margin: 0;
8
+ }
9
+
10
+ #title {
11
+ float: left;
12
+ padding: 0;
13
+ margin: 0;
14
+ font-size: 1.2rem;
15
+ }
16
+
17
+ #title:hover {
18
+ color: #007bff !important;
19
+ }
20
+
21
+ #inbox {
22
+ overflow-y: auto;
23
+ height: 100%;
24
+ max-height: 100%;
25
+ display: none;
26
+ width: 35rem;
27
+ max-width: 35rem;
28
+ padding-right: 0.2rem;
29
+ }
30
+
31
+ #inbox.visible {
32
+ display: block;
33
+ }
34
+
35
+ #inbox li span.timestamp {
36
+ font-size: 0.8rem;
37
+ margin-right: -0.5rem;
38
+ float: right;
39
+ color: #aaa;
40
+ position: absolute;
41
+ right: 1rem;
42
+ top: 0.8rem;
43
+ }
44
+
45
+ #inbox li a {
46
+ text-decoration: none;
47
+ background-color: transparent;
48
+ max-width: 22rem;
49
+ overflow: hidden;
50
+ display: block;
51
+ white-space: nowrap;
52
+ text-overflow: ellipsis
53
+ }
54
+
55
+ #inbox li a .unread-icon {
56
+ color: #007bff;
57
+ }
58
+
59
+ #inbox li a:visited .unread-icon {
60
+ color: #ffffff00;
61
+ }
62
+
63
+ #inbox li a:hover {
64
+ text-decoration: none;
65
+ }
66
+
67
+ #inbox li.active a {
68
+ color: #fff;
69
+ }
70
+
71
+ #inbox li.active span.timestamp {
72
+ color: #fff;
73
+ }
74
+
75
+ .content {
76
+ background-color: #efefef;
77
+ padding: 1rem 2rem;
78
+ height: 100%;
79
+ }
80
+
81
+ .headers table {
82
+ background-color: #fff;
83
+ }
84
+
85
+ .preview {
86
+ display: none;
87
+ background-color: #fff;
88
+ padding: 2rem;
89
+ height: 100%;
90
+ }
91
+
92
+ .preview.source-view {
93
+ padding: 1.5rem;
94
+ }
95
+
96
+ .preview iframe {
97
+ border-style: none;
98
+ width: 100%;
99
+ height: 100%;
100
+ }
101
+
102
+ .main-row {
103
+ height: 100%;
104
+ }
105
+
106
+ .container {
107
+ height: 90vh;
108
+ margin-top: 2.8rem;
109
+ }
110
+
111
+ .container.full-width {
112
+ max-width: 100%;
113
+ }
114
+
115
+ .visible {
116
+ display: block;
117
+ }
118
+
119
+ .toolbar {
120
+ background-color: #fff;
121
+ text-align: right;
122
+ padding: 0.5rem 1rem;
123
+ position: fixed;
124
+ width: 100%;
125
+ }
126
+
127
+ .toolbar i {
128
+ font-size: 1.2rem;
129
+ cursor: pointer;
130
+ margin: 0 0.2rem;
131
+ }
132
+
133
+ .toolbar i.disabled {
134
+ cursor: default;
135
+ color: #ddd !important;
136
+ }
137
+
138
+ .toolbar .separator {
139
+ border-right: 1px solid #ccc;
140
+ margin-right: 0.5rem;
141
+ margin-left: 0.2rem;
142
+ }
143
+
144
+ .right-column {
145
+ display: flex;
146
+ flex-flow: column;
147
+ height: 100%;
148
+ }
149
+
150
+ #index-iframe, #identity-iframe {
151
+ display: none;
152
+ }
153
+
154
+ .hidden {
155
+ display: none;
156
+ }
157
+
158
+ #download-link:hover {
159
+ color: #007bff !important;
160
+ }
data/layout/layout.js ADDED
@@ -0,0 +1,268 @@
1
+ (function () {
2
+ let previousInbox;
3
+ let currentView;
4
+ let reloadIdentityIframeTimeout;
5
+ let identityUuid;
6
+ let indexUuid;
7
+ let inboxInitialized = false;
8
+
9
+ var htmlIframeDocument = document.querySelector("#html-iframe").contentDocument;
10
+ var indexIframe = document.querySelector("#index-iframe");
11
+ var identityIframe = document.querySelector("#identity-iframe");
12
+ var textIframeDocument = document.querySelector("#text-iframe").contentDocument;
13
+ var sourceIframeDocument = document.querySelector("#source-iframe").contentDocument;
14
+ var sourceHighlightBundle = [
15
+ '<link rel="stylesheet"',
16
+ ' href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/agate.min.css"',
17
+ ' integrity="sha512-mMMMPADD4HAIogAWZbv+WjZTC0itafUZNI0jQm48PMBTApXt11omF5jhS7U1kp3R2Pr6oGJ+JwQKiUkUwCQaUQ=="',
18
+ ' crossorigin="anonymous" />',
19
+ '<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/highlight.min.js"',
20
+ ' integrity="sha512-tHQeqtcNWlZtEh8As/4MmZ5qpy0wj04svWFK7MIzLmUVIzaHXS8eod9OmHxyBL1UET5Rchvw7Ih4ZDv5JojZww=="',
21
+ ' crossorigin="anonymous"></' + 'script>',
22
+ '<script>hljs.initHighlightingOnLoad();</' + 'script>',
23
+ ].join('\n');
24
+
25
+ const initialize = () => {
26
+ loadMail(initialData);
27
+
28
+ reloadIdentityIframeTimeout = setTimeout(() => identityIframe.src += '', 3000);
29
+
30
+ window.addEventListener('message', function (ev) {
31
+ switch (ev.data.type) {
32
+ case 'index':
33
+ renderInbox(ev.data.uuid, ev.data.mails);
34
+ break;
35
+ case 'identity':
36
+ compareIdentity(ev.data.uuid);
37
+ break;
38
+ };
39
+ });
40
+
41
+ setInterval(function () { indexIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
42
+ setInterval(function () { identityIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
43
+
44
+ toolbar.html.onclick = function (ev) { setView('html', ev); };
45
+ toolbar.text.onclick = function (ev) { setView('text', ev); };
46
+ toolbar.source.onclick = function (ev) { setView('source', ev); };
47
+ toolbar.headers.onclick = function () { setHeadersView(!headersView); };
48
+ columnSwitch.onclick = function () { setColumnView(!twoColumnView); };
49
+
50
+ if (hasHtml) {
51
+ setView('html');
52
+ } else {
53
+ setView('text');
54
+ }
55
+
56
+ setColumnView(false);
57
+ setHeadersView(true);
58
+
59
+ $('[data-toggle="tooltip"]').tooltip();
60
+ }
61
+
62
+
63
+ var twoColumnView;
64
+ var headersView;
65
+ var headers = document.querySelector('.headers');
66
+ var inbox = document.querySelector('#inbox');
67
+ var columnSwitch = document.querySelector('.column-switch');
68
+ var headersViewSwitch = document.querySelector('.headers-view-switch');
69
+
70
+ var setHeadersView = function (enableHeadersView) {
71
+ headersView = enableHeadersView;
72
+ if (enableHeadersView) {
73
+ setOn(headersViewSwitch);
74
+ headers.classList.add('visible');
75
+ } else {
76
+ setOff(headersViewSwitch);
77
+ headers.classList.remove('visible');
78
+ }
79
+ };
80
+
81
+ var setColumnView = function (enableTwoColumnView) {
82
+ if (!inboxInitialized) return;
83
+
84
+ var container = document.querySelector('.container');
85
+ twoColumnView = enableTwoColumnView;
86
+ if (twoColumnView) {
87
+ setVisible(inbox, true);
88
+ setOn(columnSwitch);
89
+ container.classList.add('full-width');
90
+ } else {
91
+ setVisible(inbox, false);
92
+ setOff(columnSwitch);
93
+ container.classList.remove('full-width');
94
+ }
95
+ };
96
+
97
+ var contexts = ['source', 'text', 'html'];
98
+
99
+ var views = {
100
+ source: document.querySelector('.source-view'),
101
+ html: document.querySelector('.html-view'),
102
+ text: document.querySelector('.text-view'),
103
+ };
104
+
105
+ var toolbar = {
106
+ source: document.querySelector('.source-view-switch'),
107
+ html: document.querySelector('.html-view-switch'),
108
+ text: document.querySelector('.text-view-switch'),
109
+ headers: document.querySelector('.headers-view-switch'),
110
+ };
111
+
112
+ var setOn = function(element) {
113
+ element.classList.add('text-primary');
114
+ element.classList.remove('text-secondary');
115
+ };
116
+
117
+ var setOff = function(element) {
118
+ element.classList.add('text-secondary');
119
+ element.classList.remove('text-primary');
120
+ };
121
+
122
+ var setDisabled = function(element) {
123
+ element.classList.add('disabled');
124
+ element.classList.remove('text-secondary');
125
+ };
126
+
127
+ var setEnabled = function(element) {
128
+ element.classList.remove('disabled');
129
+ element.classList.add('text-secondary');
130
+ };
131
+
132
+ var setVisible = function(element, visible) {
133
+ if (visible) {
134
+ element.classList.add('visible');
135
+ } else {
136
+ element.classList.remove('visible');
137
+ }
138
+ };
139
+
140
+ var setView = function(context, ev) {
141
+ if (ev && $(ev.target).hasClass('disabled')) return;
142
+ var key;
143
+ for (i = 0; i < contexts.length; i++) {
144
+ key = contexts[i];
145
+ if (key === context) {
146
+ setOn(toolbar[key]);
147
+ setVisible(views[key], true);
148
+ } else {
149
+ setOff(toolbar[key]);
150
+ setVisible(views[key], false);
151
+ }
152
+ }
153
+ currentView = context;
154
+ };
155
+
156
+ const arrayIdentical = (a, b) => {
157
+ if (a && !b) return false;
158
+ if (a.length !== b.length) return false;
159
+ if (!a.every((item, index) => item === b[index])) return false;
160
+
161
+ return true;
162
+ };
163
+
164
+ const htmlEscape = (html) => {
165
+ return $("<div></div>").text(html).html();
166
+ };
167
+
168
+ const $headersTemplate = $("#headers-template");
169
+
170
+ const loadHeaders = (mail) => {
171
+ $template = $headersTemplate.clone();
172
+ $('#headers').html($template.html());
173
+ ['subject', 'from', 'replyTo', 'to', 'cc', 'bcc'].forEach(item => {
174
+ const $item = $(`#email-${item}`)
175
+ $item.text(mail[item]);
176
+ if (!mail[item]) $item.parent().addClass('hidden');
177
+ });
178
+ };
179
+
180
+ const loadToolbar = (mail) => {
181
+ if (!mail.textBody) {
182
+ setDisabled(toolbar.text);
183
+ setView('html');
184
+ } else {
185
+ setEnabled(toolbar.text);
186
+ }
187
+
188
+ if (!mail.htmlBody) {
189
+ setDisabled(toolbar.html);
190
+ setDisabled(toolbar.source);
191
+ setView('text');
192
+ } else {
193
+ setEnabled(toolbar.html);
194
+ setEnabled(toolbar.source);
195
+ }
196
+
197
+ setDisabled(columnSwitch);
198
+ setView(currentView);
199
+ };
200
+
201
+ const loadMail = (mail) => {
202
+ htmlIframeDocument.open();
203
+ htmlIframeDocument.write(mail.htmlBody);
204
+ htmlIframeDocument.close();
205
+
206
+ textIframeDocument.open();
207
+ textIframeDocument.write(`<pre>${htmlEscape(mail.textBody)}</pre>`);
208
+ textIframeDocument.close();
209
+
210
+ sourceIframeDocument.open();
211
+ sourceIframeDocument.write(`<pre><code style="padding: 1rem;" class="language-html">${htmlEscape(mail.htmlBody)}</code></pre>`);
212
+ sourceIframeDocument.write(sourceHighlightBundle);
213
+ sourceIframeDocument.close();
214
+
215
+ loadHeaders(mail);
216
+ loadToolbar(mail);
217
+ loadDownloadLink();
218
+ };
219
+
220
+ const loadDownloadLink = () => {
221
+ const blob = new Blob([document.documentElement.innerHTML], { type: 'application/octet-stream' });
222
+ const uri = window.URL.createObjectURL(blob);
223
+ $("#download-link").attr('href', uri);
224
+ };
225
+
226
+ const compareIdentity = (uuid) => {
227
+ clearTimeout(reloadIdentityIframeTimeout);
228
+ reloadIdentityIframeTimeout = setTimeout(() => identityIframe.src += '', 3000);
229
+ if (identityUuid !== uuid) indexIframe.src += '';
230
+ identityUuid = uuid;
231
+ };
232
+
233
+ const renderInbox = (uuid, mails) => {
234
+ if (uuid === indexUuid) {
235
+ return;
236
+ }
237
+ const mailsById = {};
238
+ const html = mails.map((mail, index) => {
239
+ const parsedTimestamp = new Date(mail.timestamp);
240
+ const timestampSpan = `<span class="timestamp">${parsedTimestamp.toLocaleString()}</span>`;
241
+ const classes = ['list-group-item', 'inbox-item'];
242
+ if (window.location.hash === '#' + mail.id) classes.push('active');
243
+ mailsById[mail.id] = mail;
244
+ return `<li data-email-id="${mail.id}" class="${classes.join(' ')}"><a title="${mail.subject}" href="javascript:void(0)">${mail.subject}</a>${timestampSpan}</li>`
245
+ });
246
+ if (arrayIdentical(html, previousInbox)) return;
247
+ previousInbox = html;
248
+ $('#inbox').html('<ul class="list-group">' + html.join('\n') + '</ul>');
249
+ $('.inbox-item').click((ev) => {
250
+ const $target = $(ev.currentTarget);
251
+ const id = $target.data('email-id');
252
+ $('.inbox-item').removeClass('active');
253
+ $target.addClass('active');
254
+ window.location.hash = id;
255
+ setTimeout(() => loadMail(mailsById[id].content), 0);
256
+ });
257
+
258
+ if (!inboxInitialized) {
259
+ setEnabled(columnSwitch);
260
+ setColumnView(true);
261
+ setVisible(inbox, true);
262
+ }
263
+ inboxInitialized = true;
264
+ indexUuid = uuid;
265
+ };
266
+
267
+ initialize();
268
+ })();
@@ -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>