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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46dd7bad9406c1b7278996f90160f228e5e287ba4c52d6a535f73cf9b40127f5
4
- data.tar.gz: 731ae1a79e1684bcbda56e065f630acf5b64a4fed8bd7191893ac0c5e050957c
3
+ metadata.gz: 530858ff7f742c65d8c002f5044caaf306efd7629e1494b27a70d7cf6da19b89
4
+ data.tar.gz: af36040935d5ea5c012ea293ee9aa459b4b3d89b51e033d02741774be9d4eb6a
5
5
  SHA512:
6
- metadata.gz: de1acd3681d97b05432820717711f2c249eadaafc3af5fef55efbe0ead0bed2fac4fb67843d2dc24e86f04a1b30515f65603e59a17951b4bf6aa0dbfcf5fdadd
7
- data.tar.gz: 12b1c99d4ae45e41b96ac70f144e1ae7d5f79c5939183aa9cb1e77bc1a9dee5d88d7e36fc89923bedd67bf27b80ff30d332e2c644b504fcb5bc017296a2df03a
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.1.3'
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.timestmap = true
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'
@@ -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
- .headers {
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="<%= mail.html_body.nil? ? 'View HTML Part (Unavailable)' : 'View HTML Part' %>"
95
- class="fa fa-code html-view-switch"></i>
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="<%= mail.html_body.nil? ? 'View HTML Source (Unavailable)' : 'View HTML Source' %>"
99
- class="fa fa-file-code-o source-view-switch"></i>
26
+ title="View HTML Part"
27
+ class="fa fa-code html-view-switch"></i>
100
28
 
101
29
  <i data-toggle="tooltip"
102
- title="<%= mail.text_body.nil? ? 'View Plaintext Part (Unavailable)' : 'View Plaintext Part' %>"
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 Email Headers"
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 headers visible">
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
- <div class="preview col source-view">
157
- <iframe id="source-iframe"></iframe>
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
- (function () {
173
- var htmlIframeDocument = document.querySelector("#html-iframe").contentDocument;
174
- htmlIframeDocument.write(<%= mail.html_body.to_s.to_json %>);
175
- htmlIframeDocument.close();
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
- %i[from reply_to to cc bcc subject text_body html_body].each do |method_name|
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
@@ -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(adapter)
9
- @adapter = adapter
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
- File.write(path, Layout.new(@adapter).content)
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
- return "#{safe_subject}.html" unless Postmortem.config.timestamp
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
- "#{timestamp}__#{safe_subject}.html"
28
+ "#{timestamp_chunk}#{token_chunk}#{safe_subject}.html"
24
29
  end
25
30
 
26
31
  def timestamp
27
- Time.now.strftime('%Y-%m-%d_%H-%M-%S')
32
+ @timestamp ||= Time.now
28
33
  end
29
34
 
30
- def safe_subject
31
- return 'no-subject' if @adapter.subject.nil? || @adapter.subject.empty?
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
- @adapter.subject.tr(' ', '_').split('').select { |char| safe_chars.include?(char) }.join
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
@@ -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(adapter)
7
- @adapter = adapter
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 = @adapter
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Postmortem
4
- VERSION = '0.1.3'
4
+ VERSION = '0.2.0'
5
5
  end
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.1.3
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-12 00:00:00.000000000 Z
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
- rubygems_version: 3.1.2
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