postmark 1.8.1 → 1.21.3
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 +5 -5
- data/.gitignore +2 -0
- data/.travis.yml +8 -5
- data/CHANGELOG.rdoc +86 -0
- data/CONTRIBUTING.md +18 -0
- data/Gemfile +6 -5
- data/LICENSE +1 -1
- data/README.md +34 -607
- data/RELEASE.md +12 -0
- data/VERSION +1 -1
- data/gemfiles/Gemfile.legacy +5 -4
- data/lib/postmark.rb +1 -18
- data/lib/postmark/account_api_client.rb +55 -1
- data/lib/postmark/api_client.rb +145 -17
- data/lib/postmark/bounce.rb +0 -4
- data/lib/postmark/client.rb +12 -4
- data/lib/postmark/error.rb +127 -0
- data/lib/postmark/handlers/mail.rb +10 -4
- data/lib/postmark/helpers/message_helper.rb +4 -0
- data/lib/postmark/http_client.rb +20 -32
- data/lib/postmark/mail_message_converter.rb +18 -5
- data/lib/postmark/message_extensions/mail.rb +83 -8
- data/lib/postmark/version.rb +1 -1
- data/postmark.gemspec +1 -1
- data/postmark.png +0 -0
- data/spec/integration/account_api_client_spec.rb +42 -10
- data/spec/integration/api_client_hashes_spec.rb +32 -49
- data/spec/integration/api_client_messages_spec.rb +33 -52
- data/spec/integration/api_client_resources_spec.rb +12 -44
- data/spec/integration/mail_delivery_method_spec.rb +21 -23
- data/spec/spec_helper.rb +4 -7
- data/spec/support/custom_matchers.rb +44 -0
- data/spec/support/shared_examples.rb +16 -16
- data/spec/unit/postmark/account_api_client_spec.rb +239 -45
- data/spec/unit/postmark/api_client_spec.rb +792 -406
- data/spec/unit/postmark/bounce_spec.rb +40 -62
- data/spec/unit/postmark/client_spec.rb +0 -6
- data/spec/unit/postmark/error_spec.rb +231 -0
- data/spec/unit/postmark/handlers/mail_spec.rb +59 -27
- data/spec/unit/postmark/helpers/hash_helper_spec.rb +5 -6
- data/spec/unit/postmark/helpers/message_helper_spec.rb +60 -11
- data/spec/unit/postmark/http_client_spec.rb +76 -61
- data/spec/unit/postmark/inbound_spec.rb +34 -34
- data/spec/unit/postmark/inflector_spec.rb +11 -13
- data/spec/unit/postmark/json_spec.rb +2 -2
- data/spec/unit/postmark/mail_message_converter_spec.rb +250 -81
- data/spec/unit/postmark/message_extensions/mail_spec.rb +249 -38
- data/spec/unit/postmark_spec.rb +37 -37
- metadata +41 -11
@@ -0,0 +1,127 @@
|
|
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 InvalidTemplateError < Error
|
103
|
+
attr_reader :postmark_response
|
104
|
+
|
105
|
+
def initialize(response)
|
106
|
+
@postmark_response = response
|
107
|
+
super('Failed to render the template. Please check #postmark_response on this error for details.')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class TimeoutError < Error
|
112
|
+
def retry?
|
113
|
+
true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class MailAdapterError < Postmark::Error; end
|
118
|
+
class UnknownMessageType < Error; end
|
119
|
+
class InvalidApiKeyError < HttpServerError; end
|
120
|
+
class InternalServerError < HttpServerError; end
|
121
|
+
class UnexpectedHttpResponseError < HttpServerError; end
|
122
|
+
|
123
|
+
# Backwards compatible aliases
|
124
|
+
DeliveryError = Error
|
125
|
+
InvalidMessageError = ApiInputError
|
126
|
+
UnknownError = UnexpectedHttpResponseError
|
127
|
+
end
|
@@ -8,10 +8,11 @@ module Mail
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def deliver!(mail)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
response = if mail.templated?
|
12
|
+
api_client.deliver_message_with_template(mail)
|
13
|
+
else
|
14
|
+
api_client.deliver_message(mail)
|
15
|
+
end
|
15
16
|
|
16
17
|
if settings[:return_response]
|
17
18
|
response
|
@@ -20,5 +21,10 @@ module Mail
|
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
24
|
+
def api_client
|
25
|
+
settings = self.settings.dup
|
26
|
+
api_token = settings.delete(:api_token) || settings.delete(:api_key)
|
27
|
+
::Postmark::ApiClient.new(api_token, settings)
|
28
|
+
end
|
23
29
|
end
|
24
30
|
end
|
@@ -18,6 +18,10 @@ module Postmark
|
|
18
18
|
message[:attachments] = attachments_to_postmark(message[:attachments])
|
19
19
|
end
|
20
20
|
|
21
|
+
if message[:track_links]
|
22
|
+
message[:track_links] = ::Postmark::Inflector.to_postmark(message[:track_links])
|
23
|
+
end
|
24
|
+
|
21
25
|
HashHelper.to_postmark(message)
|
22
26
|
end
|
23
27
|
|
data/lib/postmark/http_client.rb
CHANGED
@@ -6,7 +6,9 @@ module Postmark
|
|
6
6
|
attr_accessor :api_token
|
7
7
|
attr_reader :http, :secure, :proxy_host, :proxy_port, :proxy_user,
|
8
8
|
:proxy_pass, :host, :port, :path_prefix,
|
9
|
-
:http_open_timeout, :http_read_timeout, :
|
9
|
+
:http_open_timeout, :http_read_timeout, :http_ssl_version,
|
10
|
+
:auth_header_name
|
11
|
+
|
10
12
|
alias_method :api_key, :api_token
|
11
13
|
alias_method :api_key=, :api_token=
|
12
14
|
|
@@ -34,6 +36,10 @@ module Postmark
|
|
34
36
|
do_request { |client| client.put(url_path(path), data, headers) }
|
35
37
|
end
|
36
38
|
|
39
|
+
def patch(path, data = '')
|
40
|
+
do_request { |client| client.patch(url_path(path), data, headers) }
|
41
|
+
end
|
42
|
+
|
37
43
|
def get(path, query = {})
|
38
44
|
do_request { |client| client.get(url_path(path + to_query_string(query)), headers) }
|
39
45
|
end
|
@@ -42,6 +48,10 @@ module Postmark
|
|
42
48
|
do_request { |client| client.delete(url_path(path + to_query_string(query)), headers) }
|
43
49
|
end
|
44
50
|
|
51
|
+
def protocol
|
52
|
+
self.secure ? 'https' : 'http'
|
53
|
+
end
|
54
|
+
|
45
55
|
protected
|
46
56
|
|
47
57
|
def apply_options(options = {})
|
@@ -49,7 +59,7 @@ module Postmark
|
|
49
59
|
DEFAULTS.merge(options).each_pair do |name, value|
|
50
60
|
instance_variable_set(:"@#{name}", value)
|
51
61
|
end
|
52
|
-
@port = options[:port] || @secure ? 443 : 80
|
62
|
+
@port = options[:port] || (@secure ? 443 : 80)
|
53
63
|
end
|
54
64
|
|
55
65
|
def to_query_string(hash)
|
@@ -57,26 +67,15 @@ module Postmark
|
|
57
67
|
"?" + hash.map { |key, value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" }.join("&")
|
58
68
|
end
|
59
69
|
|
60
|
-
def protocol
|
61
|
-
self.secure ? "https" : "http"
|
62
|
-
end
|
63
|
-
|
64
70
|
def url
|
65
71
|
URI.parse("#{protocol}://#{self.host}:#{self.port}/")
|
66
72
|
end
|
67
73
|
|
68
74
|
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)
|
75
|
+
if response.code.to_i == 200
|
76
|
+
Postmark::Json.decode(response.body)
|
78
77
|
else
|
79
|
-
raise
|
78
|
+
raise HttpServerError.build(response.code, response.body)
|
80
79
|
end
|
81
80
|
end
|
82
81
|
|
@@ -92,8 +91,10 @@ module Postmark
|
|
92
91
|
@request_mutex.synchronize do
|
93
92
|
handle_response(yield(http))
|
94
93
|
end
|
95
|
-
rescue Timeout::Error
|
96
|
-
raise TimeoutError.new(
|
94
|
+
rescue Timeout::Error => e
|
95
|
+
raise TimeoutError.new(e)
|
96
|
+
rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError => e
|
97
|
+
raise HttpClientError.new(e.message)
|
97
98
|
end
|
98
99
|
|
99
100
|
def build_http
|
@@ -105,21 +106,8 @@ module Postmark
|
|
105
106
|
http.read_timeout = self.http_read_timeout
|
106
107
|
http.open_timeout = self.http_open_timeout
|
107
108
|
http.use_ssl = !!self.secure
|
108
|
-
http.ssl_version =
|
109
|
+
http.ssl_version = self.http_ssl_version if self.http_ssl_version && http.respond_to?(:ssl_version=)
|
109
110
|
http
|
110
111
|
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
112
|
end
|
125
113
|
end
|
@@ -7,10 +7,10 @@ module Postmark
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def run
|
10
|
-
delete_blank_fields(convert)
|
10
|
+
delete_blank_fields(pick_fields(convert, @message.templated?))
|
11
11
|
end
|
12
12
|
|
13
|
-
|
13
|
+
private
|
14
14
|
|
15
15
|
def convert
|
16
16
|
headers_part.merge(content_part)
|
@@ -30,10 +30,25 @@ module Postmark
|
|
30
30
|
'Subject' => @message.subject,
|
31
31
|
'Headers' => @message.export_headers,
|
32
32
|
'Tag' => @message.tag.to_s,
|
33
|
-
'TrackOpens' => cast_to_bool(@message.track_opens)
|
33
|
+
'TrackOpens' => (cast_to_bool(@message.track_opens) unless @message.track_opens.empty?),
|
34
|
+
'TrackLinks' => (::Postmark::Inflector.to_postmark(@message.track_links) unless @message.track_links.empty?),
|
35
|
+
'Metadata' => @message.metadata,
|
36
|
+
'TemplateAlias' => @message.template_alias,
|
37
|
+
'TemplateModel' => @message.template_model,
|
38
|
+
'MessageStream' => @message.message_stream
|
34
39
|
}
|
35
40
|
end
|
36
41
|
|
42
|
+
def pick_fields(message_hash, templated = false)
|
43
|
+
fields = if templated
|
44
|
+
%w(Subject HtmlBody TextBody)
|
45
|
+
else
|
46
|
+
%w(TemplateAlias TemplateModel)
|
47
|
+
end
|
48
|
+
fields.each { |key| message_hash.delete(key) }
|
49
|
+
message_hash
|
50
|
+
end
|
51
|
+
|
37
52
|
def content_part
|
38
53
|
{
|
39
54
|
'Attachments' => @message.export_attachments,
|
@@ -42,8 +57,6 @@ module Postmark
|
|
42
57
|
}
|
43
58
|
end
|
44
59
|
|
45
|
-
protected
|
46
|
-
|
47
60
|
def cast_to_bool(val)
|
48
61
|
if val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
49
62
|
val
|
@@ -15,12 +15,34 @@ module Mail
|
|
15
15
|
header['TAG'] = val
|
16
16
|
end
|
17
17
|
|
18
|
+
def track_links(val = nil)
|
19
|
+
self.track_links=(val) unless val.nil?
|
20
|
+
header['TRACK-LINKS'].to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def track_links=(val)
|
24
|
+
header['TRACK-LINKS'] = ::Postmark::Inflector.to_postmark(val)
|
25
|
+
end
|
26
|
+
|
18
27
|
def track_opens(val = nil)
|
19
|
-
|
28
|
+
self.track_opens=(val) unless val.nil?
|
29
|
+
header['TRACK-OPENS'].to_s
|
20
30
|
end
|
21
31
|
|
22
32
|
def track_opens=(val)
|
23
|
-
header['TRACK-OPENS'] = !!val
|
33
|
+
header['TRACK-OPENS'] = (!!val).to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def metadata(val = nil)
|
37
|
+
if val
|
38
|
+
@metadata = val
|
39
|
+
else
|
40
|
+
@metadata ||= {}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def metadata=(val)
|
45
|
+
@metadata = val
|
24
46
|
end
|
25
47
|
|
26
48
|
def postmark_attachments=(value)
|
@@ -39,6 +61,59 @@ module Mail
|
|
39
61
|
::Postmark::MessageHelper.attachments_to_postmark(@_attachments)
|
40
62
|
end
|
41
63
|
|
64
|
+
def template_alias(val = nil)
|
65
|
+
return self[:postmark_template_alias] && self[:postmark_template_alias].to_s if val.nil?
|
66
|
+
self[:postmark_template_alias] = val
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_writer :template_model
|
70
|
+
def template_model(model = nil)
|
71
|
+
return @template_model if model.nil?
|
72
|
+
@template_model = model
|
73
|
+
end
|
74
|
+
|
75
|
+
def message_stream(val = nil)
|
76
|
+
self.message_stream = val unless val.nil?
|
77
|
+
header['MESSAGE-STREAM'].to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
def message_stream=(val)
|
81
|
+
header['MESSAGE-STREAM'] = val
|
82
|
+
end
|
83
|
+
|
84
|
+
def templated?
|
85
|
+
!!template_alias
|
86
|
+
end
|
87
|
+
|
88
|
+
def prerender
|
89
|
+
raise ::Postmark::Error, 'Cannot prerender a message without an associated template alias' unless templated?
|
90
|
+
|
91
|
+
unless delivery_method.is_a?(::Mail::Postmark)
|
92
|
+
raise ::Postmark::MailAdapterError, "Cannot render templates via #{delivery_method.class} adapter."
|
93
|
+
end
|
94
|
+
|
95
|
+
client = delivery_method.api_client
|
96
|
+
template = client.get_template(template_alias)
|
97
|
+
response = client.validate_template(template.merge(:test_render_model => template_model || {}))
|
98
|
+
|
99
|
+
raise ::Postmark::InvalidTemplateError, response unless response[:all_content_is_valid]
|
100
|
+
|
101
|
+
self.body = nil
|
102
|
+
|
103
|
+
subject response[:subject][:rendered_content]
|
104
|
+
|
105
|
+
text_part do
|
106
|
+
body response[:text_body][:rendered_content]
|
107
|
+
end
|
108
|
+
|
109
|
+
html_part do
|
110
|
+
content_type 'text/html; charset=UTF-8'
|
111
|
+
body response[:html_body][:rendered_content]
|
112
|
+
end
|
113
|
+
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
42
117
|
def text?
|
43
118
|
if defined?(super)
|
44
119
|
super
|
@@ -77,10 +152,8 @@ module Mail
|
|
77
152
|
[].tap do |headers|
|
78
153
|
self.header.fields.each do |field|
|
79
154
|
key, value = field.name, field.value
|
80
|
-
next if
|
81
|
-
|
82
|
-
|
83
|
-
headers << { "Name" => name, "Value" => value }
|
155
|
+
next if reserved_headers.include? key.downcase
|
156
|
+
headers << { "Name" => key, "Value" => value }
|
84
157
|
end
|
85
158
|
end
|
86
159
|
end
|
@@ -107,7 +180,7 @@ module Mail
|
|
107
180
|
end
|
108
181
|
end
|
109
182
|
|
110
|
-
def
|
183
|
+
def reserved_headers
|
111
184
|
%q[
|
112
185
|
return-path x-pm-rcpt
|
113
186
|
from reply-to
|
@@ -116,7 +189,9 @@ module Mail
|
|
116
189
|
cc bcc
|
117
190
|
subject tag
|
118
191
|
attachment to
|
119
|
-
track-opens
|
192
|
+
track-opens track-links
|
193
|
+
postmark-template-alias
|
194
|
+
message-stream
|
120
195
|
]
|
121
196
|
end
|
122
197
|
|