postmark 1.10.0 → 1.11.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
  SHA1:
3
- metadata.gz: 0783a6fc070a9e3aa81200e4e37df4b4ac7cc615
4
- data.tar.gz: 945107372cad40489c8ceb05537429114d1e9e1a
3
+ metadata.gz: b07a5971093718b77a48cb6a5917167ba39d2d6e
4
+ data.tar.gz: a3f227ff7b37009da8a05fef39ae9a971fefe990
5
5
  SHA512:
6
- metadata.gz: 69edcd10b9a8b14ea2d211150992af00387438870424a5d3adf015e409f11fe67baa924cf10963a524dadbd258ef0df244cb5086f1106c3f1a0662a3e9f29bbb
7
- data.tar.gz: a5818e954ca24d7098166a5a8c63e445b0e58b763766c901ead97fe835176433ae1924d4ecb9b5b933b1c53547583a2af1d5c5577069c6d9285866633c35e691
6
+ metadata.gz: 1dc9c40d2e9c7410c371d681cebcf90d27b25898c932a729d6dabb8f9391e9060cfa38d8a36b9abde7218ca0be45d8d048af36e91bfeb456980c9690fe8f3ee3
7
+ data.tar.gz: ef0e6f6cb70ba23b0ad301eac397a2f26287b66131986c2bce421d0b9a5dffc66538d4f53c644db54791de0b0689777234436048d2e5a11f473bdb2480d47221
@@ -1,5 +1,12 @@
1
1
  = Changelog
2
2
 
3
+ == 1.11.0
4
+
5
+ * New, improved, and backwards-compatible gem errors (see README).
6
+ * Added support for retrieving message clicks using the Messages API.
7
+ * Added support for sending templated message in batches.
8
+ * Added support for assigning link tracking mode via `Mail::Message` headers.
9
+
3
10
  == 1.10.0
4
11
 
5
12
  * Fix a bug when open tracking flag is set to false by default, when open tracking flag is not set by a user.
@@ -0,0 +1,18 @@
1
+ # Before you report an issue or submit a pull request
2
+
3
+ *If you are blocked or need help with Postmark, please [contact
4
+ Postmark Support](https://postmarkapp.com/contact)*. For other, non-urgent
5
+ cases you’re welcome to report a bug and/or contribute to this project. We will
6
+ make our best effort to review your contributions and triage any bug reports in
7
+ a timely fashion.
8
+
9
+ If you’d like to submit a pull request:
10
+
11
+ * Fork the project.
12
+ * Make your feature addition or bug fix.
13
+ * Add tests for it. This is important to prevent future regressions.
14
+ * Do not mess with rakefile, version, or history.
15
+ * Update the CHANGELOG, list your changes under Unreleased.
16
+ * Update the README if necessary.
17
+ * Write short, descriptive commit messages, following the format used in therepo.
18
+ * Send a pull request. Bonus points for topic branches.
data/Gemfile CHANGED
@@ -4,11 +4,12 @@ source "http://rubygems.org"
4
4
  gemspec
5
5
 
6
6
  group :test do
7
- gem 'rspec', '~> 2.14.0'
8
- gem 'fakeweb'
7
+ gem 'rspec', '~> 3.7'
8
+ gem 'rspec-its', '~> 1.2'
9
+ gem 'fakeweb', :git => 'https://github.com/chrisk/fakeweb.git'
9
10
  gem 'fakeweb-matcher'
10
- gem 'mime-types', '~> 1.25.1'
11
- gem 'activesupport', '~> 3.2.0'
11
+ gem 'mime-types'
12
+ gem 'activesupport'
12
13
  gem 'i18n', '~> 0.6.0'
13
14
  gem 'yajl-ruby', '~> 1.0', :platforms => [:mingw, :mswin, :ruby]
14
- end
15
+ end
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Petyo Ivanov
1
+ Copyright (c) 2018 Wildbit LLC.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -660,6 +660,66 @@ bounce.activate # reactivate hard bounce
660
660
  # => #<Postmark::Bounce:0x007ff09c04ae18 @id=580516117, @email="sheldon@bigbangtheory.com", @bounced_at=2012-10-21 00:01:56 +0800, @type="HardBounce", @name=nil, @details="smtp;550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient's email address for typos or unnecessary spaces. Learn more at http://support.google.com/mail/bin/answer.py?answer=6596 c13si5382730vcw.23", @tag=nil, @dump_available=false, @inactive=true, @can_activate=true, @message_id="876d40fe-ab2a-4925-9d6f-8d5e4f4926f5", @subject="Re: What, to you, is a large crowd?">
661
661
  ```
662
662
 
663
+ ## Error handling
664
+
665
+ For the gem version `1.11.0` and above, use the following template to handle the errors you care about:
666
+
667
+ ``` ruby
668
+ def handle_postmark_errors
669
+ # Any Postmark request
670
+ yield
671
+ error
672
+ rescue Postmark::InvalidApiKeyError => error
673
+ # Authentication error
674
+ # TODO: Make sure your API token is correct
675
+ puts error
676
+ error
677
+ rescue Postmark::TimeoutError => error
678
+ # Network timeout, auto-retried :max_retries times
679
+ # TODO: Save message locally, try again once the network issues are resolved
680
+ # Consider increasing `http_open_timeout` and `http_read_timeout`.
681
+ puts error
682
+ error
683
+ rescue Postmark::InternalServerError => error
684
+ # Postmark server error, auto-retried :max_retries times
685
+ # TODO: Save message locally, try again later.
686
+ puts error
687
+ error
688
+ rescue Postmark::HttpClientError => error
689
+ # Corrupted response from Postmark, auto-retried :max_retries times
690
+ # TODO: Save message locally, try again later.
691
+ puts error
692
+ error
693
+ rescue Postmark::InactiveRecipientError => error
694
+ # You tried to send to one or more recipients marked as inactive in
695
+ # Postmark
696
+ # TODO: Mark listed recipients as inactive in your local db or reactivate
697
+ # using the Bounces API
698
+ puts "Inactive recipients: #{error.recipients.join(', ')}"
699
+ puts error
700
+ error
701
+ rescue Postmark::ApiInputError => error
702
+ # Postmark rejected your request as invalid
703
+ # TODO: Look up the error code and resolve the problem in your app
704
+ # List of supported error codes:
705
+ # https://postmarkapp.com/developer/api/overview#error-codes
706
+ puts "#{error.error_code} #{error.message}"
707
+ puts error
708
+ error
709
+ rescue Postmark::Error => error
710
+ # All other Postmark errors
711
+ # TODO: Log and review as needed
712
+ puts error
713
+ error
714
+ rescue Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED,
715
+ EOFError, Net::ProtocolError, SocketError => error
716
+ # Standard Ruby network errors, auto-retried :max_reties times
717
+ # TODO: Save message locally, resolve network issues, try again.
718
+ puts error
719
+ error
720
+ end
721
+ ```
722
+
663
723
  ## Requirements
664
724
 
665
725
  You will need a Postmark account, server and sender signature set up to use it.
@@ -678,15 +738,8 @@ Postmark.response_parser_class = :Json # :ActiveSupport or :Yajl are also suppor
678
738
 
679
739
  ## Note on Patches/Pull Requests
680
740
 
681
- * Fork the project.
682
- * Make your feature addition or bug fix.
683
- * Add tests for it. This is important to prevent future regressions.
684
- * Do not mess with rakefile, version, or history
685
- * Update the CHANGELOG, list your changes under Unreleased.
686
- * Update the README if necessary.
687
- * Write short, descriptive commit messages, following the format used in the repo.
688
- * Send a pull request. Bonus points for topic branches.
741
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
689
742
 
690
743
  ## Copyright
691
744
 
692
- Copyright © 2016 Wildbit LLC. See LICENSE for details.
745
+ Copyright © 2018 Wildbit LLC. See LICENSE for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.10.0
1
+ 1.11.0
@@ -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
16
  gem 'yajl-ruby', '~> 1.0', :platforms => [:mingw, :mswin, :ruby]
16
- end
17
+ end
@@ -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'
@@ -113,22 +113,42 @@ module Postmark
113
113
  find_each('messages/outbound/opens', 'Opens', options)
114
114
  end
115
115
 
116
+ def clicks(options = {})
117
+ find_each('messages/outbound/clicks', 'Clicks', options)
118
+ end
119
+
116
120
  def get_opens(options = {})
117
121
  _, batch = load_batch('messages/outbound/opens', 'Opens', options)
118
122
  batch
119
123
  end
120
124
 
121
- def get_opens_by_message_id(message_id, options ={})
125
+ def get_clicks(options = {})
126
+ _, batch = load_batch('messages/outbound/clicks', 'Clicks', options)
127
+ batch
128
+ end
129
+
130
+ def get_opens_by_message_id(message_id, options = {})
122
131
  _, batch = load_batch("messages/outbound/opens/#{message_id}",
123
132
  'Opens',
124
133
  options)
125
134
  batch
126
135
  end
127
136
 
137
+ def get_clicks_by_message_id(message_id, options = {})
138
+ _, batch = load_batch("messages/outbound/clicks/#{message_id}",
139
+ 'Clicks',
140
+ options)
141
+ batch
142
+ end
143
+
128
144
  def opens_by_message_id(message_id, options = {})
129
145
  find_each("messages/outbound/opens/#{message_id}", 'Opens', options)
130
146
  end
131
147
 
148
+ def clicks_by_message_id(message_id, options = {})
149
+ find_each("messages/outbound/clicks/#{message_id}", 'Clicks', options)
150
+ end
151
+
132
152
  def create_trigger(type, options)
133
153
  data = serialize(HashHelper.to_postmark(options))
134
154
  format_response http_client.post("triggers/#{type}", data)
@@ -221,6 +241,17 @@ module Postmark
221
241
  end
222
242
  end
223
243
 
244
+ def deliver_in_batches_with_templates(message_hashes)
245
+ in_batches(message_hashes) do |batch, offset|
246
+ mapped = batch.map { |h| MessageHelper.to_postmark(h) }
247
+ data = serialize(:Messages => mapped)
248
+
249
+ with_retries do
250
+ http_client.post('email/batchWithTemplates', data)
251
+ end
252
+ end
253
+ end
254
+
224
255
  def get_stats_totals(options = {})
225
256
  format_response(http_client.get('stats/outbound', options))
226
257
  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
 
@@ -0,0 +1,117 @@
1
+ module Postmark
2
+ class Error < ::StandardError; end
3
+
4
+ class HttpClientError < Error
5
+ def retry?
6
+ true
7
+ end
8
+ end
9
+
10
+ class HttpServerError < Error
11
+ attr_accessor :status_code, :parsed_body, :body
12
+
13
+ alias_method :full_response, :parsed_body
14
+
15
+ def self.build(status_code, body)
16
+ parsed_body = Postmark::Json.decode(body) rescue {}
17
+
18
+ case status_code
19
+ when '401'
20
+ InvalidApiKeyError.new(401, body, parsed_body)
21
+ when '422'
22
+ ApiInputError.build(body, parsed_body)
23
+ when '500'
24
+ InternalServerError.new(500, body, parsed_body)
25
+ else
26
+ UnexpectedHttpResponseError.new(status_code, body, parsed_body)
27
+ end
28
+ end
29
+
30
+ def initialize(status_code = 500, body = '', parsed_body = {})
31
+ self.parsed_body = parsed_body
32
+ self.status_code = status_code.to_i
33
+ message = parsed_body.fetch(
34
+ 'Message',
35
+ "The Postmark API responded with HTTP status #{status_code}.")
36
+
37
+ super(message)
38
+ end
39
+
40
+ def retry?
41
+ 5 == status_code / 100
42
+ end
43
+ end
44
+
45
+ class ApiInputError < HttpServerError
46
+ INACTIVE_RECIPIENT = 406
47
+
48
+ attr_accessor :error_code
49
+
50
+ def self.build(body, parsed_body)
51
+ error_code = parsed_body['ErrorCode'].to_i
52
+
53
+ case error_code
54
+ when INACTIVE_RECIPIENT
55
+ InactiveRecipientError.new(INACTIVE_RECIPIENT, body, parsed_body)
56
+ else
57
+ new(error_code, body, parsed_body)
58
+ end
59
+ end
60
+
61
+ def initialize(error_code = nil, body = '', parsed_body = {})
62
+ self.error_code = error_code.to_i
63
+ super(422, body, parsed_body)
64
+ end
65
+
66
+ def retry?
67
+ false
68
+ end
69
+ end
70
+
71
+ class InactiveRecipientError < ApiInputError
72
+ attr_reader :recipients
73
+
74
+ PATTERNS = [/^Found inactive addresses: (.+?)\.$/.freeze,
75
+ /^Found inactive addresses: (.+?)\.$/.freeze,
76
+ /these inactive addresses: (.+?)\. Inactive/.freeze].freeze
77
+
78
+ def self.parse_recipients(message)
79
+ PATTERNS.each do |p|
80
+ _, recipients = p.match(message).to_a
81
+ next unless recipients
82
+ return recipients.split(', ')
83
+ end
84
+
85
+ []
86
+ end
87
+
88
+ def initialize(*args)
89
+ super
90
+ @recipients = parse_recipients || []
91
+ end
92
+
93
+ private
94
+
95
+ def parse_recipients
96
+ return unless parsed_body && !parsed_body.empty?
97
+
98
+ self.class.parse_recipients(parsed_body['Message'])
99
+ end
100
+ end
101
+
102
+ class TimeoutError < Error
103
+ def retry?
104
+ true
105
+ end
106
+ end
107
+
108
+ class UnknownMessageType < Error; end
109
+ class InvalidApiKeyError < HttpServerError; end
110
+ class InternalServerError < HttpServerError; end
111
+ class UnexpectedHttpResponseError < HttpServerError; end
112
+
113
+ # Backwards compatible aliases
114
+ DeliveryError = Error
115
+ InvalidMessageError = ApiInputError
116
+ UnknownError = UnexpectedHttpResponseError
117
+ end
@@ -66,17 +66,10 @@ module Postmark
66
66
  end
67
67
 
68
68
  def handle_response(response)
69
- case response.code.to_i
70
- when 200
71
- return Postmark::Json.decode(response.body)
72
- when 401
73
- raise error(InvalidApiKeyError, response.body)
74
- when 422
75
- raise error(InvalidMessageError, response.body)
76
- when 500
77
- raise error(InternalServerError, response.body)
69
+ if response.code.to_i == 200
70
+ Postmark::Json.decode(response.body)
78
71
  else
79
- raise UnknownError, response
72
+ raise HttpServerError.build(response.code, response.body)
80
73
  end
81
74
  end
82
75
 
@@ -92,8 +85,10 @@ module Postmark
92
85
  @request_mutex.synchronize do
93
86
  handle_response(yield(http))
94
87
  end
95
- rescue Timeout::Error
96
- raise TimeoutError.new($!)
88
+ rescue Timeout::Error => e
89
+ raise TimeoutError.new(e)
90
+ rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError => e
91
+ raise HttpClientError.new(e.message)
97
92
  end
98
93
 
99
94
  def build_http
@@ -108,18 +103,5 @@ module Postmark
108
103
  http.ssl_version = :TLSv1 if http.respond_to?(:ssl_version=)
109
104
  http
110
105
  end
111
-
112
- def error_message(response_body)
113
- Postmark::Json.decode(response_body)["Message"]
114
- end
115
-
116
- def error_message_and_code(response_body)
117
- reply = Postmark::Json.decode(response_body)
118
- [reply["Message"], reply["ErrorCode"], reply]
119
- end
120
-
121
- def error(clazz, response_body)
122
- clazz.send(:new, *error_message_and_code(response_body))
123
- end
124
106
  end
125
107
  end