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.
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
@@ -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
- settings = self.settings.dup
12
- api_token = settings.delete(:api_token) || settings.delete(:api_key)
13
- api_client = ::Postmark::ApiClient.new(api_token, settings)
14
- response = api_client.deliver_message(mail)
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
 
@@ -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, :auth_header_name
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
- 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)
75
+ if response.code.to_i == 200
76
+ Postmark::Json.decode(response.body)
78
77
  else
79
- raise UnknownError, response
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 = :TLSv1 if http.respond_to?(: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
- protected
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
- default 'TRACK-OPENS', !!val
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 bogus_headers.include? key.downcase
81
- name = key.split(/-/).map { |i| i.capitalize }.join('-')
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 bogus_headers
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