postmark 1.10.0 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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