postmark 1.8.1 → 1.21.3

Sign up to get free protection for your applications and to get access to all the features.
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