postmortem 0.1.2 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e05359d9e039f6e44c0e89decaac3c5c72fe3447f7a96fdb8ac1667fe93d537
4
- data.tar.gz: 651d2b986f6b9b07e8fe311cd163865490005da3376c95c4ce05a4110a313e62
3
+ metadata.gz: 530858ff7f742c65d8c002f5044caaf306efd7629e1494b27a70d7cf6da19b89
4
+ data.tar.gz: af36040935d5ea5c012ea293ee9aa459b4b3d89b51e033d02741774be9d4eb6a
5
5
  SHA512:
6
- metadata.gz: 66a929b9cfa7043ebce54eda09c4168e4adfd485d24e32720dc68238c6ea578030c89ad90181d770b22b33d358c2945bae12cc5df9a8f3d3019989b6a72cc8ca
7
- data.tar.gz: 82478c5ef6a04531776023947b39b3c1c04a003185a602199a4460e2ba4b6759a87493475666f4056d0bcc759b7de3a5765db8b3356d28e863e71ed445ca7e2a
6
+ metadata.gz: b21c568f8b4a0668a97d43b296d26b86434c5aabb33af5327d0ab4c59b5375f81f632ff29e8eab5c2fb05a8db591392f97fe709fa980c266660a99a4cdcd0ec4
7
+ data.tar.gz: def87f9d2630d6dfd6bdd8315e6e1b3865f81edf4e8918e24a4ebcd5669cf96dbd0f606f5c9d30249787d32be51e62318790dede38f14d850a074eb47fef0aa5
@@ -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.2'
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_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_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>
@@ -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
+ }
@@ -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
+ })();
@@ -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)
@@ -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.2'
4
+ VERSION = '0.2.0'
5
5
  end
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.2
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-09 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