postmortem 0.1.0 → 0.1.1

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: 396ef931001e6084271eb46994b2f9ec65daa948f544cf75306e28c0aab1c6df
4
- data.tar.gz: 851cd36f02f31b91152ffbcc7cad143c911078ac9f5dd991f9345f23728030d2
3
+ metadata.gz: 7d675b984abb71e212b6c7f3e190c9bf8d17282ed0741b260cf06891cff5b70d
4
+ data.tar.gz: 408e888943fe644d76b686d3693588378c513138ea88d82da633bf0c2367f19e
5
5
  SHA512:
6
- metadata.gz: 42a525ba6a1d6744177591da24b03585daab8476945dbbbfa7d3f6b3cc8202db19804507e40e5415b05c2edb8eefa793a9f7eb710094595b47d832b169f7055a
7
- data.tar.gz: 7e8386b1af03787689640fb5ad812fadbe3e04429471c17f166220bfc6f6e57b18bc7eb8f722b9dd22d6031cae892c846941e48b9881fc5214c6f6332849c373
6
+ metadata.gz: c6cddb8f06e21fd028b7531d79efabf8320e86a22670cfed45901b189d563249cd1a420859ff8cbf1a7a542a15d8c908018a6fb8477ec023407997e7d51338e9
7
+ data.tar.gz: cfda5e45b5302a36cbd7503b418142c13f5c0e458ca0ba1e705503513b9549e5c41d440af485bade2db280a1890fffef5df33bfd487250e38762ee7a811b4992
data/.gitignore CHANGED
@@ -2,7 +2,6 @@
2
2
  /.yardoc
3
3
  /_yardoc/
4
4
  /coverage/
5
- /doc/
6
5
  /pkg/
7
6
  /spec/reports/
8
7
  /tmp/
@@ -10,3 +9,5 @@
10
9
  # rspec failure tracking
11
10
  .rspec_status
12
11
  *.gem
12
+
13
+ Gemfile.lock
@@ -1,6 +1,7 @@
1
1
  Metrics/BlockLength:
2
2
  Exclude:
3
3
  - 'spec/**/*_spec.rb'
4
+ - 'postmortem.gemspec'
4
5
 
5
6
  Layout/EmptyLinesAroundAttributeAccessor:
6
7
  Enabled: true
data/README.md CHANGED
@@ -1,15 +1,29 @@
1
1
  # Postmortem
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/postmortem`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ _Postmortem_ provides a simple and clean preview of all outgoing mails sent by your _Ruby_ application to make email development a little less painful.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ Every time your application sends an email a clearly-visible log entry will be written which provides a path to a temporary file containing your preview.
6
+
7
+ Take a look at a [live example](https://postmortem.surge.sh/) to see _Postmortem_ in action.
8
+
9
+ _Postmortem_ should only be enabled in test or development environments.
10
+
11
+ ## Features
12
+
13
+ * Seamless integration with [_ActionMailer_](https://guides.rubyonrails.org/action_mailer_basics.html) and [_Pony_](https://github.com/benprew/pony) (with automatic delivery skipping when using _Pony_).
14
+ * Preview email content as well as typical email headers (recipients, subject, etc.).
15
+ * View rendered _HTML_, plaintext, or _HTML_ source with syntax highlighting (courtesy of [highlight.js](https://highlightjs.org/)).
16
+ * Dual or single column view to suit your requirements.
17
+ * Content is loaded inside an `<iframe>` to ensure document isolation and validity.
6
18
 
7
19
  ## Installation
8
20
 
9
- Add this line to your application's Gemfile:
21
+ Add the gem to your application's Gemfile:
10
22
 
11
23
  ```ruby
12
- gem 'postmortem'
24
+ group :development, :test do
25
+ gem 'postmortem', '~> 0.1.1'
26
+ end
13
27
  ```
14
28
 
15
29
  And then execute:
@@ -22,18 +36,58 @@ Or install it yourself as:
22
36
 
23
37
  ## Usage
24
38
 
25
- TODO: Write usage instructions here
39
+ _Postmortem_ automatically integrates with _Rails ActionMailer_ and _Pony_. When an email is sent an entry will be visible in your application's log output.
40
+
41
+ The path to the preview file is based on the current time and the subject of the email. If you would prefer to use the same path for each email you can disable timestamps (see [configuration](#configuration)) and simply reload your browser every time an email is sent.
26
42
 
27
- ## Development
43
+ If you are using assets (images etc.) with _ActionMailer_ make sure to configure the asset host, e.g.:
28
44
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
45
+ ```ruby
46
+ # config/environments/development.rb
47
+ Rails.application.configure do
48
+ config.action_mailer.asset_host = 'http://localhost:3000'
49
+ end
50
+ ```
30
51
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
52
+ Load the provided file in your browser to preview your email.
32
53
 
33
- ## Contributing
54
+ ![Screenshot](doc/screenshot.png)
55
+
56
+
57
+ ## Configuration
58
+ <a name="configuration"></a>
34
59
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/postmortem.
60
+ Configure _Postmortem_ by calling `Postmortem.configure`, e.g. in a _Rails_ initializer.
61
+
62
+ ```ruby
63
+ # config/initializers/postmortem.rb
64
+ Postmortem.configure do |config|
65
+ # Colorize output in logs (path to preview HTML file) to improve visibility (default: true).
66
+ config.colorize = true
67
+
68
+ # Prefix all preview filenames with a timestamp (default: true).
69
+ # Setting to false allows refreshing the same path in your browser to view the latest version.
70
+ config.timestmap = true
71
+
72
+ # Path to the Postmortem log file, where preview paths are written (default: STDOUT).
73
+ config.log_path = '/path/to/postmortem.log'
74
+
75
+ # Path to save preview .html files (default: OS-provided temp directory).
76
+ # The directory will be created if it does not exist.
77
+ config.preview_directory = '/path/to/postmortem/directory'
78
+
79
+ # Provide a custom layout path if the default interface does not suit you.
80
+ # See `layout/default.html.erb` for implementation reference.
81
+ config.layout = '/path/to/layout'
82
+
83
+ # Skip delivery of emails when using Pony (default: true).
84
+ config.pony_skip_delivery = true
85
+ end
86
+ ```
87
+
88
+ ## Contributing
36
89
 
90
+ Feel free to make a pull request.
37
91
 
38
92
  ## License
39
93
 
Binary file
@@ -1,49 +1,290 @@
1
1
  <html>
2
2
  <head>
3
- <link rel="stylesheet"
4
- href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
5
- integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
6
- crossorigin="anonymous">
3
+ <link rel="stylesheet"
4
+ href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
5
+ integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
6
+ crossorigin="anonymous" />
7
+ <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
8
+ rel="stylesheet" />
9
+ <link rel="stylesheet"
10
+ href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/a11y-light.min.css"
11
+ integrity="sha512-zqEpZCxg7IVhGXy6EwdTb26cHl8IZzN/29Mj2/oSzkCKiLxHTi491mcC/K1NhShMWXu+WvfU5Z261XPc60lw7g=="
12
+ crossorigin="anonymous" />
13
+ <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
+ }
88
+ </style>
7
89
  </head>
8
- <body style="background-color: #efefef; padding: 2rem; height: 100%">
9
- <div style="height: 100%; display: flex; flex-direction: column">
10
- <table class="table table-hover table-bordered" style="background-color: #fff">
11
- <tbody>
12
- <tr>
13
- <th style="width: 7rem;">From:</th>
14
- <td><%= mail.from&.join(', ') %></td>
15
- </tr>
16
-
17
- <tr>
18
- <th>To:</th>
19
- <td><%= mail.to&.join(', ') %></td>
20
- </tr>
21
-
22
- <tr>
23
- <th>Cc:</th>
24
- <td><%= mail.cc&.join(', ') %></td>
25
- </tr>
26
-
27
- <tr>
28
- <th>Bcc:</th>
29
- <td><%= mail.bcc&.join(', ') %></td>
30
- </tr>
31
-
32
- <tr>
33
- <th>Subject:</th>
34
- <td><%= mail.subject %></td>
35
- </tr>
36
- </tbody>
37
- </table>
38
-
39
- <div style="background-color: #ffffff; padding: 2rem; height: 100%; flex-grow: 1">
40
- <iframe id="mail-iframe" src="" style="border-style: none; width: 100%; height: 100%"></iframe>
90
+ <body>
91
+ <div class="toolbar">
92
+
93
+ <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>
96
+
97
+ <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>
100
+
101
+ <i data-toggle="tooltip"
102
+ title="<%= mail.text_body.nil? ? 'View Plaintext Part (Unavailable)' : 'View Plaintext Part' %>"
103
+ class="fa fa-file-text-o text-view-switch"></i>
104
+
105
+ <span class="separator"></span>
106
+
107
+ <i data-toggle="tooltip"
108
+ title="Toggle Email Headers"
109
+ class="fa fa-columns column-switch"></i>
110
+
41
111
  </div>
112
+ <div class="content">
113
+ <div class="container full-width">
114
+ <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>
155
+ </div>
156
+ <div class="preview col source-view">
157
+ <iframe id="source-iframe"></iframe>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
163
+ integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
164
+ crossorigin="anonymous"></script>
165
+ <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
166
+ integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN"
167
+ crossorigin="anonymous"></script>
168
+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"
169
+ integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV"
170
+ crossorigin="anonymous"></script>
42
171
  <script>
43
- var mailContent = <%= mail.html_body.to_json %>;
44
- var iframeDocument = document.querySelector("#mail-iframe").contentDocument;
45
- iframeDocument.write(mailContent);
46
- iframeDocument.close();
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
+ })();
47
288
  </script>
48
289
  </body>
49
290
  </html>
@@ -7,27 +7,20 @@ require 'fileutils'
7
7
  require 'mail'
8
8
  require 'erb'
9
9
  require 'json'
10
+ require 'cgi'
10
11
 
11
12
  require 'postmortem/version'
12
13
  require 'postmortem/adapters'
13
14
  require 'postmortem/delivery'
14
15
  require 'postmortem/layout'
16
+ require 'postmortem/configuration'
15
17
 
16
18
  # HTML email inspection tool.
17
19
  module Postmortem
18
20
  class Error < StandardError; end
19
21
 
20
22
  class << self
21
- attr_reader :output_directory, :layout
22
- attr_accessor :output_file
23
-
24
- def output_directory=(val)
25
- @output_directory = Pathname.new(val)
26
- end
27
-
28
- def layout=(val)
29
- @layout = Pathname.new(val)
30
- end
23
+ attr_reader :config
31
24
 
32
25
  def record_delivery(mail)
33
26
  Delivery.new(mail)
@@ -35,15 +28,20 @@ module Postmortem
35
28
  .tap { |delivery| log_delivery(delivery) }
36
29
  end
37
30
 
38
- def try_load(*args)
31
+ def try_load(*args, plugin:)
39
32
  args.each { |arg| require arg }
40
33
  rescue LoadError
41
34
  false
42
35
  else
43
- yield
36
+ require "postmortem/plugins/#{plugin}"
44
37
  true
45
38
  end
46
39
 
40
+ def configure
41
+ @config = Configuration.new
42
+ yield @config if block_given?
43
+ end
44
+
47
45
  private
48
46
 
49
47
  def log_delivery(delivery)
@@ -52,14 +50,19 @@ module Postmortem
52
50
  end
53
51
 
54
52
  def colorized(val)
55
- return val unless output_file.tty?
53
+ return val unless output_file.tty? || !config.colorize
56
54
 
57
55
  "\e[34m[postmortem]\e[36m #{val}\e[0m"
58
56
  end
57
+
58
+ def output_file
59
+ return STDOUT if config.log_path.nil?
60
+
61
+ @output_file ||= File.open(config.log_path, mode: 'a')
62
+ end
59
63
  end
60
64
  end
61
65
 
62
- Postmortem.output_directory = File.join(Dir.tmpdir, 'postmortem')
63
- Postmortem.output_file = STDOUT
64
- Postmortem.layout = File.expand_path(File.join(__dir__, '..', 'layout', 'default.html.erb'))
65
- Postmortem.try_load('action_mailer', 'active_support') { require 'postmortem/action_mailer' }
66
+ Postmortem.configure
67
+ Postmortem.try_load('action_mailer', 'active_support', plugin: 'action_mailer')
68
+ Postmortem.try_load('pony', plugin: 'pony')
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'postmortem/adapters/base'
4
4
  require 'postmortem/adapters/action_mailer'
5
+ require 'postmortem/adapters/pony'
5
6
 
6
7
  module Postmortem
7
8
  # Adapters for various email senders (e.g. ActionMailer).
@@ -6,16 +6,55 @@ module Postmortem
6
6
  class ActionMailer < Base
7
7
  private
8
8
 
9
- def adapted(data)
9
+ def adapted
10
10
  {
11
- from: data[:from],
12
- to: data[:to],
13
- cc: data[:cc],
14
- bcc: data[:bcc],
15
- subject: data[:subject],
16
- html_body: Mail.new(data[:mail]).html_part.decoded.strip
11
+ from: mail.from,
12
+ reply_to: mail.reply_to,
13
+ to: mail.to,
14
+ cc: mail.cc,
15
+ bcc: normalized_bcc,
16
+ subject: mail.subject,
17
+ text_body: text_part,
18
+ html_body: html_part
17
19
  }
18
20
  end
21
+
22
+ def text_part
23
+ return nil unless text?
24
+ return mail.body.decoded unless mail.text_part
25
+
26
+ mail.text_part.decoded
27
+ end
28
+
29
+ def html_part
30
+ return nil unless html?
31
+ return mail.body.decoded unless mail.html_part
32
+
33
+ mail.html_part.decoded
34
+ end
35
+
36
+ def mail
37
+ @mail ||= Mail.new(@data[:mail])
38
+ end
39
+
40
+ def normalized_bcc
41
+ Mail.new(to: @data[:bcc]).to
42
+ end
43
+
44
+ def text?
45
+ return true unless mail.has_content_type?
46
+ return true if mail.content_type.include?('text/plain')
47
+ return true if mail.multipart? && mail.text_part
48
+
49
+ false
50
+ end
51
+
52
+ def html?
53
+ return true if mail.has_content_type? && mail.content_type.include?('text/html')
54
+ return true if mail.multipart? && mail.html_part
55
+
56
+ false
57
+ end
19
58
  end
20
59
  end
21
60
  end
@@ -5,20 +5,20 @@ module Postmortem
5
5
  # Base interface implementation for all Postmortem adapters.
6
6
  class Base
7
7
  def initialize(data)
8
- @mail = adapted(data)
8
+ @data = data
9
+ @adapted = adapted
9
10
  end
10
11
 
11
- %i[from to cc bcc subject html_body].each do |method_name|
12
+ %i[from reply_to to cc bcc subject text_body html_body].each do |method_name|
12
13
  define_method method_name do
13
- @mail[method_name]
14
+ @adapted[method_name]
14
15
  end
15
16
  end
16
17
 
17
18
  private
18
19
 
19
- def adapted(_data)
20
- raise NotImplementedError,
21
- 'Adapter must be a child class of Base which implements #adapted'
20
+ def adapted
21
+ raise NotImplementedError, 'Adapter child class must implement #adapted'
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postmortem
4
+ module Adapters
5
+ # Pony adapter.
6
+ class Pony < Base
7
+ private
8
+
9
+ def adapted
10
+ {
11
+ from: mail.from,
12
+ reply_to: mail.reply_to,
13
+ to: mail.to,
14
+ cc: mail.cc,
15
+ bcc: mail.bcc,
16
+ subject: mail.subject,
17
+ text_body: @data[:body],
18
+ html_body: @data[:html_body]
19
+ }
20
+ end
21
+
22
+ def mail
23
+ @mail ||= Mail.new(@data.select { |key| keys.include?(key) })
24
+ end
25
+
26
+ def keys
27
+ %i[from reply_to to cc bcc subject text_body html_body]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postmortem
4
+ # Provides interface for configuring Postmortem and implements sensible defaults.
5
+ class Configuration
6
+ attr_writer :colorize, :timestamp, :pony_skip_delivery
7
+ attr_accessor :log_path
8
+
9
+ def timestamp
10
+ defined?(@timestamp) ? @timestamp : true
11
+ end
12
+
13
+ def colorize
14
+ defined?(@colorize) ? @colorize : true
15
+ end
16
+
17
+ def preview_directory=(val)
18
+ @preview_directory = Pathname.new(val)
19
+ end
20
+
21
+ def layout=(val)
22
+ @layout = Pathname.new(val)
23
+ end
24
+
25
+ def layout
26
+ default = File.expand_path(File.join(__dir__, '..', '..', 'layout', 'default'))
27
+ path = Pathname.new(@layout || default)
28
+ path.extname.empty? ? path.sub_ext('.html.erb') : path
29
+ end
30
+
31
+ def preview_directory
32
+ @preview_directory ||= Pathname.new(File.join(Dir.tmpdir, 'postmortem'))
33
+ end
34
+
35
+ def pony_skip_delivery
36
+ defined?(@pony_skip_delivery) ? @pony_skip_delivery : true
37
+ end
38
+ end
39
+ end
@@ -7,7 +7,7 @@ module Postmortem
7
7
 
8
8
  def initialize(adapter)
9
9
  @adapter = adapter
10
- @path = Postmortem.output_directory.join(filename)
10
+ @path = Postmortem.config.preview_directory.join(filename)
11
11
  end
12
12
 
13
13
  def record
@@ -18,6 +18,8 @@ module Postmortem
18
18
  private
19
19
 
20
20
  def filename
21
+ return "#{safe_subject}.html" unless Postmortem.config.timestamp
22
+
21
23
  "#{timestamp}__#{safe_subject}.html"
22
24
  end
23
25
 
@@ -26,7 +28,7 @@ module Postmortem
26
28
  end
27
29
 
28
30
  def safe_subject
29
- return 'no-subject' if @adapter.subject.empty?
31
+ return 'no-subject' if @adapter.subject.nil? || @adapter.subject.empty?
30
32
 
31
33
  @adapter.subject.tr(' ', '_').split('').select { |char| safe_chars.include?(char) }.join
32
34
  end
@@ -7,9 +7,13 @@ module Postmortem
7
7
  @adapter = adapter
8
8
  end
9
9
 
10
+ def format_email_array(array)
11
+ array&.map { |email| %(<a href="mailto:#{email}">#{email}</a>) }&.join(', ')
12
+ end
13
+
10
14
  def content
11
15
  mail = @adapter
12
- ERB.new(Postmortem.layout.read).result(binding)
16
+ ERB.new(Postmortem.config.layout.read).result(binding)
13
17
  end
14
18
  end
15
19
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pony monkey-patch.
4
+ module Pony
5
+ class << self
6
+ alias _pony_mail mail
7
+
8
+ def mail(options)
9
+ result = _pony_mail(options) unless Postmortem.config.pony_skip_delivery
10
+ Postmortem.record_delivery(Postmortem::Adapters::Pony.new(options))
11
+ result
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Postmortem
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  end
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.add_runtime_dependency 'mail', '~> 2.7'
29
29
 
30
30
  spec.add_development_dependency 'actionmailer', '~> 6.0'
31
+ spec.add_development_dependency 'pony', '~> 1.13'
31
32
  spec.add_development_dependency 'rspec', '~> 3.9'
32
33
  spec.add_development_dependency 'rspec-its', '~> 1.3'
33
34
  spec.add_development_dependency 'rubocop', '~> 0.88.0'
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.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-15 00:00:00.000000000 Z
11
+ date: 2020-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pony
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.13'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rspec
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -119,21 +133,24 @@ files:
119
133
  - ".rspec"
120
134
  - ".rubocop.yml"
121
135
  - Gemfile
122
- - Gemfile.lock
123
136
  - LICENSE.txt
124
137
  - Makefile
125
138
  - README.md
126
139
  - Rakefile
127
140
  - bin/console
128
141
  - bin/setup
142
+ - doc/screenshot.png
129
143
  - layout/default.html.erb
130
144
  - lib/postmortem.rb
131
- - lib/postmortem/action_mailer.rb
132
145
  - lib/postmortem/adapters.rb
133
146
  - lib/postmortem/adapters/action_mailer.rb
134
147
  - lib/postmortem/adapters/base.rb
148
+ - lib/postmortem/adapters/pony.rb
149
+ - lib/postmortem/configuration.rb
135
150
  - lib/postmortem/delivery.rb
136
151
  - lib/postmortem/layout.rb
152
+ - lib/postmortem/plugins/action_mailer.rb
153
+ - lib/postmortem/plugins/pony.rb
137
154
  - lib/postmortem/version.rb
138
155
  - postmortem.gemspec
139
156
  homepage: https://github.com/bobf/postmortem
@@ -1,126 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- postmortem (0.1.0)
5
- mail (~> 2.7)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- actionmailer (6.0.3.2)
11
- actionpack (= 6.0.3.2)
12
- actionview (= 6.0.3.2)
13
- activejob (= 6.0.3.2)
14
- mail (~> 2.5, >= 2.5.4)
15
- rails-dom-testing (~> 2.0)
16
- actionpack (6.0.3.2)
17
- actionview (= 6.0.3.2)
18
- activesupport (= 6.0.3.2)
19
- rack (~> 2.0, >= 2.0.8)
20
- rack-test (>= 0.6.3)
21
- rails-dom-testing (~> 2.0)
22
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
23
- actionview (6.0.3.2)
24
- activesupport (= 6.0.3.2)
25
- builder (~> 3.1)
26
- erubi (~> 1.4)
27
- rails-dom-testing (~> 2.0)
28
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
29
- activejob (6.0.3.2)
30
- activesupport (= 6.0.3.2)
31
- globalid (>= 0.3.6)
32
- activesupport (6.0.3.2)
33
- concurrent-ruby (~> 1.0, >= 1.0.2)
34
- i18n (>= 0.7, < 2)
35
- minitest (~> 5.1)
36
- tzinfo (~> 1.1)
37
- zeitwerk (~> 2.2, >= 2.2.2)
38
- ast (2.4.1)
39
- builder (3.2.4)
40
- concurrent-ruby (1.1.7)
41
- crass (1.0.6)
42
- devpack (0.2.1)
43
- diff-lcs (1.4.4)
44
- erubi (1.9.0)
45
- globalid (0.4.2)
46
- activesupport (>= 4.2.0)
47
- i18n (1.8.5)
48
- concurrent-ruby (~> 1.0)
49
- loofah (2.6.0)
50
- crass (~> 1.0.2)
51
- nokogiri (>= 1.5.9)
52
- mail (2.7.1)
53
- mini_mime (>= 0.1.1)
54
- mini_mime (1.0.2)
55
- mini_portile2 (2.4.0)
56
- minitest (5.14.1)
57
- nokogiri (1.10.10)
58
- mini_portile2 (~> 2.4.0)
59
- paint (2.2.0)
60
- parallel (1.19.2)
61
- parser (2.7.1.4)
62
- ast (~> 2.4.1)
63
- rack (2.2.3)
64
- rack-test (1.1.0)
65
- rack (>= 1.0, < 3)
66
- rails-dom-testing (2.0.3)
67
- activesupport (>= 4.2.0)
68
- nokogiri (>= 1.6)
69
- rails-html-sanitizer (1.3.0)
70
- loofah (~> 2.3)
71
- rainbow (3.0.0)
72
- regexp_parser (1.7.1)
73
- rexml (3.2.4)
74
- rspec (3.9.0)
75
- rspec-core (~> 3.9.0)
76
- rspec-expectations (~> 3.9.0)
77
- rspec-mocks (~> 3.9.0)
78
- rspec-core (3.9.2)
79
- rspec-support (~> 3.9.3)
80
- rspec-expectations (3.9.2)
81
- diff-lcs (>= 1.2.0, < 2.0)
82
- rspec-support (~> 3.9.0)
83
- rspec-its (1.3.0)
84
- rspec-core (>= 3.0.0)
85
- rspec-expectations (>= 3.0.0)
86
- rspec-mocks (3.9.1)
87
- diff-lcs (>= 1.2.0, < 2.0)
88
- rspec-support (~> 3.9.0)
89
- rspec-support (3.9.3)
90
- rubocop (0.88.0)
91
- parallel (~> 1.10)
92
- parser (>= 2.7.1.1)
93
- rainbow (>= 2.2.2, < 4.0)
94
- regexp_parser (>= 1.7)
95
- rexml
96
- rubocop-ast (>= 0.1.0, < 1.0)
97
- ruby-progressbar (~> 1.7)
98
- unicode-display_width (>= 1.4.0, < 2.0)
99
- rubocop-ast (0.3.0)
100
- parser (>= 2.7.1.4)
101
- ruby-progressbar (1.10.1)
102
- strong_versions (0.4.5)
103
- i18n (>= 0.5)
104
- paint (~> 2.0)
105
- thread_safe (0.3.6)
106
- timecop (0.9.1)
107
- tzinfo (1.2.7)
108
- thread_safe (~> 0.1)
109
- unicode-display_width (1.7.0)
110
- zeitwerk (2.4.0)
111
-
112
- PLATFORMS
113
- ruby
114
-
115
- DEPENDENCIES
116
- actionmailer (~> 6.0)
117
- devpack (~> 0.2.0)
118
- postmortem!
119
- rspec (~> 3.9)
120
- rspec-its (~> 1.3)
121
- rubocop (~> 0.88.0)
122
- strong_versions (~> 0.4.5)
123
- timecop (~> 0.9.1)
124
-
125
- BUNDLED WITH
126
- 2.1.4