postmark 1.8.1 → 1.21.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/.travis.yml +8 -5
  4. data/CHANGELOG.rdoc +86 -0
  5. data/CONTRIBUTING.md +18 -0
  6. data/Gemfile +6 -5
  7. data/LICENSE +1 -1
  8. data/README.md +34 -607
  9. data/RELEASE.md +12 -0
  10. data/VERSION +1 -1
  11. data/gemfiles/Gemfile.legacy +5 -4
  12. data/lib/postmark.rb +1 -18
  13. data/lib/postmark/account_api_client.rb +55 -1
  14. data/lib/postmark/api_client.rb +145 -17
  15. data/lib/postmark/bounce.rb +0 -4
  16. data/lib/postmark/client.rb +12 -4
  17. data/lib/postmark/error.rb +127 -0
  18. data/lib/postmark/handlers/mail.rb +10 -4
  19. data/lib/postmark/helpers/message_helper.rb +4 -0
  20. data/lib/postmark/http_client.rb +20 -32
  21. data/lib/postmark/mail_message_converter.rb +18 -5
  22. data/lib/postmark/message_extensions/mail.rb +83 -8
  23. data/lib/postmark/version.rb +1 -1
  24. data/postmark.gemspec +1 -1
  25. data/postmark.png +0 -0
  26. data/spec/integration/account_api_client_spec.rb +42 -10
  27. data/spec/integration/api_client_hashes_spec.rb +32 -49
  28. data/spec/integration/api_client_messages_spec.rb +33 -52
  29. data/spec/integration/api_client_resources_spec.rb +12 -44
  30. data/spec/integration/mail_delivery_method_spec.rb +21 -23
  31. data/spec/spec_helper.rb +4 -7
  32. data/spec/support/custom_matchers.rb +44 -0
  33. data/spec/support/shared_examples.rb +16 -16
  34. data/spec/unit/postmark/account_api_client_spec.rb +239 -45
  35. data/spec/unit/postmark/api_client_spec.rb +792 -406
  36. data/spec/unit/postmark/bounce_spec.rb +40 -62
  37. data/spec/unit/postmark/client_spec.rb +0 -6
  38. data/spec/unit/postmark/error_spec.rb +231 -0
  39. data/spec/unit/postmark/handlers/mail_spec.rb +59 -27
  40. data/spec/unit/postmark/helpers/hash_helper_spec.rb +5 -6
  41. data/spec/unit/postmark/helpers/message_helper_spec.rb +60 -11
  42. data/spec/unit/postmark/http_client_spec.rb +76 -61
  43. data/spec/unit/postmark/inbound_spec.rb +34 -34
  44. data/spec/unit/postmark/inflector_spec.rb +11 -13
  45. data/spec/unit/postmark/json_spec.rb +2 -2
  46. data/spec/unit/postmark/mail_message_converter_spec.rb +250 -81
  47. data/spec/unit/postmark/message_extensions/mail_spec.rb +249 -38
  48. data/spec/unit/postmark_spec.rb +37 -37
  49. metadata +41 -11
data/RELEASE.md ADDED
@@ -0,0 +1,12 @@
1
+ New versions of the gem are cut by the Postmark team, this is a quick guide to ensuring a smooth release.
2
+
3
+ 1. Verify all builds are passing on Travis for your branch.
4
+ 1. Merge in your branch to master.
5
+ 1. Update VERSION and lib/postmark/version.rb with the new version.
6
+ 1. Update CHANGELOG.rdoc with a brief description of the changes.
7
+ 1. Commit to git with a comment of "Bump version to x.y.z".
8
+ 1. run `rake release` - This will push to github(with the version tag) and rubygems with the version in lib/postmark/version.rb.
9
+ *Note that if you're on Bundler 1.17 there's a bug that hides the prompt for your OTP. If it hangs after adding the tag then it's asking for your OTP, enter your OTP and press Enter. Bundler 2.x and beyond resolved this issue. *
10
+ 1. Verify the new version is on [github](https://github.com/wildbit/postmark-gem) and [rubygems](https://rubygems.org/gems/postmark).
11
+ 1. Create a new release for the version on [Github releases](https://github.com/wildbit/postmark-gem/releases).
12
+ 1. Add or update any related content to the [wiki](https://github.com/wildbit/postmark-gem/wiki).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.1
1
+ 1.21.3
@@ -6,11 +6,12 @@ gem 'rake', '< 11.0.0'
6
6
  gem 'json', '< 2.0.0'
7
7
 
8
8
  group :test do
9
- gem 'rspec', '~> 2.14.0'
10
- gem 'fakeweb'
9
+ gem 'rspec', '~> 3.7'
10
+ gem 'rspec-its', '~> 1.2'
11
+ gem 'fakeweb', :git => 'https://github.com/chrisk/fakeweb.git'
11
12
  gem 'fakeweb-matcher'
12
13
  gem 'mime-types', '~> 1.25.1'
13
14
  gem 'activesupport', '~> 3.2.0'
14
15
  gem 'i18n', '~> 0.6.0'
15
- gem 'yajl-ruby', '~> 1.0', :platforms => [:mingw, :mswin, :ruby]
16
- end
16
+ gem 'yajl-ruby', '~> 1.0', '< 1.4.0', :platforms => [:mingw, :mswin, :ruby]
17
+ end
data/lib/postmark.rb CHANGED
@@ -10,6 +10,7 @@ require 'postmark/mail_message_converter'
10
10
  require 'postmark/bounce'
11
11
  require 'postmark/inbound'
12
12
  require 'postmark/json'
13
+ require 'postmark/error'
13
14
  require 'postmark/http_client'
14
15
  require 'postmark/client'
15
16
  require 'postmark/api_client'
@@ -18,24 +19,6 @@ require 'postmark/message_extensions/mail'
18
19
  require 'postmark/handlers/mail'
19
20
 
20
21
  module Postmark
21
-
22
- class DeliveryError < StandardError
23
- attr_accessor :error_code, :full_response
24
-
25
- def initialize(message = nil, error_code = nil, full_response = nil)
26
- super(message)
27
- self.error_code = error_code
28
- self.full_response = full_response
29
- end
30
- end
31
-
32
- class UnknownError < DeliveryError; end
33
- class InvalidApiKeyError < DeliveryError; end
34
- class InvalidMessageError < DeliveryError; end
35
- class InternalServerError < DeliveryError; end
36
- class UnknownMessageType < DeliveryError; end
37
- class TimeoutError < DeliveryError; end
38
-
39
22
  module ResponseParsers
40
23
  autoload :Json, 'postmark/response_parsers/json'
41
24
  autoload :ActiveSupport, 'postmark/response_parsers/active_support'
@@ -61,6 +61,54 @@ module Postmark
61
61
  end
62
62
  alias_method :delete_signature, :delete_sender
63
63
 
64
+ def domains(options = {})
65
+ find_each('domains', 'Domains', options)
66
+ end
67
+
68
+ def get_domains(options = {})
69
+ load_batch('domains', 'Domains', options).last
70
+ end
71
+
72
+ def get_domains_count(options = {})
73
+ get_resource_count('domains', options)
74
+ end
75
+
76
+ def get_domain(id)
77
+ format_response http_client.get("domains/#{id.to_i}")
78
+ end
79
+
80
+ def create_domain(attributes = {})
81
+ data = serialize(HashHelper.to_postmark(attributes))
82
+
83
+ format_response http_client.post('domains', data)
84
+ end
85
+
86
+ def update_domain(id, attributes = {})
87
+ data = serialize(HashHelper.to_postmark(attributes))
88
+
89
+ format_response http_client.put("domains/#{id.to_i}", data)
90
+ end
91
+
92
+ def verify_domain_dkim(id)
93
+ format_response http_client.put("domains/#{id.to_i}/verifydkim")
94
+ end
95
+
96
+ def verify_domain_return_path(id)
97
+ format_response http_client.put("domains/#{id.to_i}/verifyreturnpath")
98
+ end
99
+
100
+ def verified_domain_spf?(id)
101
+ !!http_client.post("domains/#{id.to_i}/verifyspf")['SPFVerified']
102
+ end
103
+
104
+ def rotate_domain_dkim(id)
105
+ format_response http_client.post("domains/#{id.to_i}/rotatedkim")
106
+ end
107
+
108
+ def delete_domain(id)
109
+ format_response http_client.delete("domains/#{id.to_i}")
110
+ end
111
+
64
112
  def servers(options = {})
65
113
  find_each('servers', 'Servers', options)
66
114
  end
@@ -91,6 +139,12 @@ module Postmark
91
139
  format_response http_client.delete("servers/#{id.to_i}")
92
140
  end
93
141
 
142
+ def push_templates(attributes = {})
143
+ data = serialize(HashHelper.to_postmark(attributes))
144
+ _, batch = format_batch_response(http_client.put('templates/push', data), "Templates")
145
+ batch
146
+ end
147
+
94
148
  end
95
149
 
96
- end
150
+ end
@@ -27,6 +27,11 @@ module Postmark
27
27
  end
28
28
 
29
29
  def deliver_message(message)
30
+ if message.templated?
31
+ raise ArgumentError,
32
+ "Please use #{self.class}#deliver_message_with_template to deliver messages with templates."
33
+ end
34
+
30
35
  data = serialize(message.to_postmark_hash)
31
36
 
32
37
  with_retries do
@@ -37,7 +42,26 @@ module Postmark
37
42
  end
38
43
  end
39
44
 
45
+ def deliver_message_with_template(message)
46
+ raise ArgumentError, 'Templated delivery requested, but the template is missing.' unless message.templated?
47
+
48
+ data = serialize(message.to_postmark_hash)
49
+
50
+ with_retries do
51
+ response, error = take_response_of { http_client.post("email/withTemplate", data) }
52
+ update_message(message, response)
53
+ raise error if error
54
+ format_response(response, true)
55
+ end
56
+ end
57
+
40
58
  def deliver_messages(messages)
59
+ if messages.any? { |m| m.templated? }
60
+ raise ArgumentError,
61
+ "Some of the provided messages have templates. Please use " \
62
+ "#{self.class}#deliver_messages_with_templates to deliver those."
63
+ end
64
+
41
65
  in_batches(messages) do |batch, offset|
42
66
  data = serialize(batch.map { |m| m.to_postmark_hash })
43
67
 
@@ -51,6 +75,24 @@ module Postmark
51
75
  end
52
76
  end
53
77
 
78
+ def deliver_messages_with_templates(messages)
79
+ unless messages.all? { |m| m.templated? }
80
+ raise ArgumentError, 'Templated delivery requested, but one or more messages lack templates.'
81
+ end
82
+
83
+ in_batches(messages) do |batch, offset|
84
+ data = serialize(batch.map { |m| m.to_postmark_hash })
85
+
86
+ with_retries do
87
+ http_client.post("email/batchWithTemplates", data).tap do |response|
88
+ response.each_with_index do |r, i|
89
+ update_message(messages[offset + i], r)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
54
96
  def delivery_stats
55
97
  response = format_response(http_client.get("deliverystats"), true)
56
98
 
@@ -93,10 +135,6 @@ module Postmark
93
135
  batch
94
136
  end
95
137
 
96
- def get_bounced_tags
97
- http_client.get("bounces/tags")
98
- end
99
-
100
138
  def get_bounce(id)
101
139
  format_response http_client.get("bounces/#{id}")
102
140
  end
@@ -113,23 +151,44 @@ module Postmark
113
151
  find_each('messages/outbound/opens', 'Opens', options)
114
152
  end
115
153
 
154
+ def clicks(options = {})
155
+ find_each('messages/outbound/clicks', 'Clicks', options)
156
+ end
157
+
116
158
  def get_opens(options = {})
117
159
  _, batch = load_batch('messages/outbound/opens', 'Opens', options)
118
160
  batch
119
161
  end
120
162
 
121
- def get_opens_by_message_id(message_id, options ={})
163
+ def get_clicks(options = {})
164
+ _, batch = load_batch('messages/outbound/clicks', 'Clicks', options)
165
+ batch
166
+ end
167
+
168
+ def get_opens_by_message_id(message_id, options = {})
122
169
  _, batch = load_batch("messages/outbound/opens/#{message_id}",
123
170
  'Opens',
124
171
  options)
125
172
  batch
126
173
  end
127
174
 
175
+ def get_clicks_by_message_id(message_id, options = {})
176
+ _, batch = load_batch("messages/outbound/clicks/#{message_id}",
177
+ 'Clicks',
178
+ options)
179
+ batch
180
+ end
181
+
128
182
  def opens_by_message_id(message_id, options = {})
129
183
  find_each("messages/outbound/opens/#{message_id}", 'Opens', options)
130
184
  end
131
185
 
186
+ def clicks_by_message_id(message_id, options = {})
187
+ find_each("messages/outbound/clicks/#{message_id}", 'Clicks', options)
188
+ end
189
+
132
190
  def create_trigger(type, options)
191
+ type = Postmark::Inflector.to_postmark(type).downcase
133
192
  data = serialize(HashHelper.to_postmark(options))
134
193
  format_response http_client.post("triggers/#{type}", data)
135
194
  end
@@ -138,22 +197,20 @@ module Postmark
138
197
  format_response http_client.get("triggers/#{type}/#{id}")
139
198
  end
140
199
 
141
- def update_trigger(type, id, options)
142
- data = serialize(HashHelper.to_postmark(options))
143
- format_response http_client.put("triggers/#{type}/#{id}", data)
144
- end
145
-
146
200
  def delete_trigger(type, id)
201
+ type = Postmark::Inflector.to_postmark(type).downcase
147
202
  format_response http_client.delete("triggers/#{type}/#{id}")
148
203
  end
149
204
 
150
205
  def get_triggers(type, options = {})
151
- _, batch = load_batch("triggers/#{type}", 'Tags', options)
206
+ type = Postmark::Inflector.to_postmark(type)
207
+ _, batch = load_batch("triggers/#{type.downcase}", type, options)
152
208
  batch
153
209
  end
154
210
 
155
211
  def triggers(type, options = {})
156
- find_each("triggers/#{type}", 'Tags', options)
212
+ type = Postmark::Inflector.to_postmark(type)
213
+ find_each("triggers/#{type.downcase}", type, options)
157
214
  end
158
215
 
159
216
  def server_info
@@ -221,22 +278,94 @@ module Postmark
221
278
  end
222
279
  end
223
280
 
281
+ def deliver_in_batches_with_templates(message_hashes)
282
+ in_batches(message_hashes) do |batch, offset|
283
+ mapped = batch.map { |h| MessageHelper.to_postmark(h) }
284
+ data = serialize(:Messages => mapped)
285
+
286
+ with_retries do
287
+ http_client.post('email/batchWithTemplates', data)
288
+ end
289
+ end
290
+ end
291
+
224
292
  def get_stats_totals(options = {})
225
293
  format_response(http_client.get('stats/outbound', options))
226
294
  end
227
295
 
228
296
  def get_stats_counts(stat, options = {})
229
297
  url = "stats/outbound/#{stat}"
230
-
231
298
  url << "/#{options[:type]}" if options.has_key?(:type)
232
299
 
233
300
  response = format_response(http_client.get(url, options))
234
-
235
301
  response[:days].map! { |d| HashHelper.to_ruby(d) }
236
-
237
302
  response
238
303
  end
239
304
 
305
+ def get_webhooks(options = {})
306
+ options = HashHelper.to_postmark(options)
307
+ _, batch = load_batch('webhooks', 'Webhooks', options)
308
+ batch
309
+ end
310
+
311
+ def get_webhook(id)
312
+ format_response http_client.get("webhooks/#{id}")
313
+ end
314
+
315
+ def create_webhook(attributes = {})
316
+ data = serialize(HashHelper.to_postmark(attributes))
317
+
318
+ format_response http_client.post('webhooks', data)
319
+ end
320
+
321
+ def update_webhook(id, attributes = {})
322
+ data = serialize(HashHelper.to_postmark(attributes))
323
+
324
+ format_response http_client.put("webhooks/#{id}", data)
325
+ end
326
+
327
+ def delete_webhook(id)
328
+ format_response http_client.delete("webhooks/#{id}")
329
+ end
330
+
331
+ def get_message_streams(options = {})
332
+ _, batch = load_batch('message-streams', 'MessageStreams', options)
333
+ batch
334
+ end
335
+
336
+ def message_streams(options = {})
337
+ find_each('message-streams', 'MessageStreams', options)
338
+ end
339
+
340
+ def get_message_stream(id)
341
+ format_response(http_client.get("message-streams/#{id}"))
342
+ end
343
+
344
+ def create_message_stream(attributes = {})
345
+ data = serialize(HashHelper.to_postmark(attributes))
346
+ format_response(http_client.post('message-streams', data))
347
+ end
348
+
349
+ def update_message_stream(id, attributes)
350
+ data = serialize(HashHelper.to_postmark(attributes))
351
+ format_response(http_client.patch("message-streams/#{id}", data))
352
+ end
353
+
354
+ def dump_suppressions(stream_id, options = {})
355
+ _, batch = load_batch("message-streams/#{stream_id}/suppressions/dump", 'Suppressions', options)
356
+ batch
357
+ end
358
+
359
+ def create_suppressions(stream_id, email_addresses)
360
+ data = serialize(:Suppressions => Array(email_addresses).map { |e| HashHelper.to_postmark(:email_address => e) })
361
+ format_response(http_client.post("message-streams/#{stream_id}/suppressions", data))
362
+ end
363
+
364
+ def delete_suppressions(stream_id, email_addresses)
365
+ data = serialize(:Suppressions => Array(email_addresses).map { |e| HashHelper.to_postmark(:email_address => e) })
366
+ format_response(http_client.post("message-streams/#{stream_id}/suppressions/delete", data))
367
+ end
368
+
240
369
  protected
241
370
 
242
371
  def in_batches(messages)
@@ -249,7 +378,7 @@ module Postmark
249
378
 
250
379
  def update_message(message, response)
251
380
  response ||= {}
252
- message['Message-ID'] = response['MessageID']
381
+ message['X-PM-Message-Id'] = response['MessageID']
253
382
  message.delivered = response['ErrorCode'] && response['ErrorCode'].zero?
254
383
  message.postmark_response = response
255
384
  end
@@ -265,6 +394,5 @@ module Postmark
265
394
  path = options.delete(:inbound) ? 'messages/inbound' : 'messages/outbound'
266
395
  [path, messages_key, options]
267
396
  end
268
-
269
397
  end
270
398
  end
@@ -54,10 +54,6 @@ module Postmark
54
54
  Bounce.new(bounce_json)
55
55
  end
56
56
  end
57
-
58
- def tags
59
- Postmark.api_client.get_bounced_tags
60
- end
61
57
  end
62
58
 
63
59
  end
@@ -37,12 +37,16 @@ module Postmark
37
37
 
38
38
  def with_retries
39
39
  yield
40
- rescue DeliveryError
40
+ rescue HttpServerError, HttpClientError, TimeoutError, Errno::EINVAL,
41
+ Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError,
42
+ Net::ProtocolError, SocketError => e
41
43
  retries = retries ? retries + 1 : 1
42
- if retries < self.max_retries
44
+ retriable = !e.respond_to?(:retry?) || e.retry?
45
+
46
+ if retriable && retries < self.max_retries
43
47
  retry
44
48
  else
45
- raise
49
+ raise e
46
50
  end
47
51
  end
48
52
 
@@ -52,7 +56,7 @@ module Postmark
52
56
 
53
57
  def take_response_of
54
58
  [yield, nil]
55
- rescue DeliveryError => e
59
+ rescue HttpServerError => e
56
60
  [e.full_response || {}, e]
57
61
  end
58
62
 
@@ -76,6 +80,10 @@ module Postmark
76
80
  options[:offset] ||= 0
77
81
  options[:count] ||= 30
78
82
  response = http_client.get(path, options)
83
+ format_batch_response(response, name)
84
+ end
85
+
86
+ def format_batch_response(response, name)
79
87
  [response['TotalCount'], format_response(response[name])]
80
88
  end
81
89