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 +4 -4
- data/CHANGELOG.rdoc +7 -0
- data/CONTRIBUTING.md +18 -0
- data/Gemfile +6 -5
- data/LICENSE +1 -1
- data/README.md +62 -9
- data/VERSION +1 -1
- data/gemfiles/Gemfile.legacy +4 -3
- data/lib/postmark.rb +1 -18
- data/lib/postmark/api_client.rb +32 -1
- data/lib/postmark/client.rb +8 -4
- data/lib/postmark/error.rb +117 -0
- data/lib/postmark/http_client.rb +7 -25
- data/lib/postmark/mail_message_converter.rb +1 -1
- data/lib/postmark/version.rb +1 -1
- data/spec/integration/account_api_client_spec.rb +2 -2
- data/spec/integration/api_client_messages_spec.rb +3 -3
- data/spec/spec_helper.rb +4 -1
- data/spec/support/custom_matchers.rb +30 -0
- data/spec/unit/postmark/account_api_client_spec.rb +22 -22
- data/spec/unit/postmark/api_client_spec.rb +164 -54
- data/spec/unit/postmark/bounce_spec.rb +10 -10
- data/spec/unit/postmark/error_spec.rb +218 -0
- data/spec/unit/postmark/handlers/mail_spec.rb +5 -5
- data/spec/unit/postmark/http_client_spec.rb +4 -5
- data/spec/unit/postmark/mail_message_converter_spec.rb +6 -0
- data/spec/unit/postmark/message_extensions/mail_spec.rb +6 -6
- data/spec/unit/postmark_spec.rb +5 -5
- metadata +33 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b07a5971093718b77a48cb6a5917167ba39d2d6e
|
4
|
+
data.tar.gz: a3f227ff7b37009da8a05fef39ae9a971fefe990
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1dc9c40d2e9c7410c371d681cebcf90d27b25898c932a729d6dabb8f9391e9060cfa38d8a36b9abde7218ca0be45d8d048af36e91bfeb456980c9690fe8f3ee3
|
7
|
+
data.tar.gz: ef0e6f6cb70ba23b0ad301eac397a2f26287b66131986c2bce421d0b9a5dffc66538d4f53c644db54791de0b0689777234436048d2e5a11f473bdb2480d47221
|
data/CHANGELOG.rdoc
CHANGED
@@ -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.
|
data/CONTRIBUTING.md
ADDED
@@ -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', '~>
|
8
|
-
gem '
|
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'
|
11
|
-
gem 'activesupport'
|
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
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
|
-
|
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 ©
|
745
|
+
Copyright © 2018 Wildbit LLC. See LICENSE for details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.11.0
|
data/gemfiles/Gemfile.legacy
CHANGED
@@ -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', '~>
|
10
|
-
gem '
|
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
|
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'
|
data/lib/postmark/api_client.rb
CHANGED
@@ -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
|
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
|
data/lib/postmark/client.rb
CHANGED
@@ -37,12 +37,16 @@ module Postmark
|
|
37
37
|
|
38
38
|
def with_retries
|
39
39
|
yield
|
40
|
-
rescue
|
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
|
-
|
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
|
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
|
data/lib/postmark/http_client.rb
CHANGED
@@ -66,17 +66,10 @@ module Postmark
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def handle_response(response)
|
69
|
-
|
70
|
-
|
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
|
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
|