Permalink
Browse files

Merge pull request #65 from wildbit/version-1.11.0

Version 1.11.0
  • Loading branch information...
temochka committed Mar 14, 2018
2 parents 053b536 + 4e97c33 commit 705818ea20353c036a89729d203be019c9b05843
View
@@ -1,6 +1,11 @@
= Changelog
+== 1.11.0
+
+* New, improved, and backwards-compatible gem errors (see README).
* Added support for retrieving message clicks using the Messages API.
+* Added support for sending templated message in batches.
+* Added support for assigning link tracking mode via `Mail::Message` headers.
== 1.10.0
View
@@ -0,0 +1,18 @@
+# Before you report an issue or submit a pull request
+
+*If you are blocked or need help with Postmark, please [contact
+Postmark Support](https://postmarkapp.com/contact)*. For other, non-urgent
+cases you’re welcome to report a bug and/or contribute to this project. We will
+make our best effort to review your contributions and triage any bug reports in
+a timely fashion.
+
+If you’d like to submit a pull request:
+
+* Fork the project.
+* Make your feature addition or bug fix.
+* Add tests for it. This is important to prevent future regressions.
+* Do not mess with rakefile, version, or history.
+* Update the CHANGELOG, list your changes under Unreleased.
+* Update the README if necessary.
+* Write short, descriptive commit messages, following the format used in therepo.
+* Send a pull request. Bonus points for topic branches.
View
@@ -1,4 +1,4 @@
-Copyright (c) 2009 Petyo Ivanov
+Copyright (c) 2018 Wildbit LLC.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
View
@@ -660,6 +660,66 @@ bounce.activate # reactivate hard bounce
# => #<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?">
```
+## Error handling
+
+For the gem version `1.11.0` and above, use the following template to handle the errors you care about:
+
+``` ruby
+def handle_postmark_errors
+ # Any Postmark request
+ yield
+ error
+rescue Postmark::InvalidApiKeyError => error
+ # Authentication error
+ # TODO: Make sure your API token is correct
+ puts error
+ error
+rescue Postmark::TimeoutError => error
+ # Network timeout, auto-retried :max_retries times
+ # TODO: Save message locally, try again once the network issues are resolved
+ # Consider increasing `http_open_timeout` and `http_read_timeout`.
+ puts error
+ error
+rescue Postmark::InternalServerError => error
+ # Postmark server error, auto-retried :max_retries times
+ # TODO: Save message locally, try again later.
+ puts error
+ error
+rescue Postmark::HttpClientError => error
+ # Corrupted response from Postmark, auto-retried :max_retries times
+ # TODO: Save message locally, try again later.
+ puts error
+ error
+rescue Postmark::InactiveRecipientError => error
+ # You tried to send to one or more recipients marked as inactive in
+ # Postmark
+ # TODO: Mark listed recipients as inactive in your local db or reactivate
+ # using the Bounces API
+ puts "Inactive recipients: #{error.recipients.join(', ')}"
+ puts error
+ error
+rescue Postmark::ApiInputError => error
+ # Postmark rejected your request as invalid
+ # TODO: Look up the error code and resolve the problem in your app
+ # List of supported error codes:
+ # https://postmarkapp.com/developer/api/overview#error-codes
+ puts "#{error.error_code} #{error.message}"
+ puts error
+ error
+rescue Postmark::Error => error
+ # All other Postmark errors
+ # TODO: Log and review as needed
+ puts error
+ error
+rescue Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED,
+ EOFError, Net::ProtocolError, SocketError => error
+ # Standard Ruby network errors, auto-retried :max_reties times
+ # TODO: Save message locally, resolve network issues, try again.
+ puts error
+ error
+end
+```
+
## Requirements
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
## Note on Patches/Pull Requests
-* Fork the project.
-* Make your feature addition or bug fix.
-* Add tests for it. This is important to prevent future regressions.
-* Do not mess with rakefile, version, or history
-* Update the CHANGELOG, list your changes under Unreleased.
-* Update the README if necessary.
-* Write short, descriptive commit messages, following the format used in the repo.
-* Send a pull request. Bonus points for topic branches.
+See [CONTRIBUTING.md](CONTRIBUTING.md).
## Copyright
-Copyright © 2016 Wildbit LLC. See LICENSE for details.
+Copyright © 2018 Wildbit LLC. See LICENSE for details.
View
@@ -1 +1 @@
-1.10.0
+1.11.0
View
@@ -10,6 +10,7 @@
require 'postmark/bounce'
require 'postmark/inbound'
require 'postmark/json'
+require 'postmark/error'
require 'postmark/http_client'
require 'postmark/client'
require 'postmark/api_client'
@@ -18,24 +19,6 @@
require 'postmark/handlers/mail'
module Postmark
-
- class DeliveryError < StandardError
- attr_accessor :error_code, :full_response
-
- def initialize(message = nil, error_code = nil, full_response = nil)
- super(message)
- self.error_code = error_code
- self.full_response = full_response
- end
- end
-
- class UnknownError < DeliveryError; end
- class InvalidApiKeyError < DeliveryError; end
- class InvalidMessageError < DeliveryError; end
- class InternalServerError < DeliveryError; end
- class UnknownMessageType < DeliveryError; end
- class TimeoutError < DeliveryError; end
-
module ResponseParsers
autoload :Json, 'postmark/response_parsers/json'
autoload :ActiveSupport, 'postmark/response_parsers/active_support'
View
@@ -241,6 +241,17 @@ def deliver_with_template(attributes = {})
end
end
+ def deliver_in_batches_with_templates(message_hashes)
+ in_batches(message_hashes) do |batch, offset|
+ mapped = batch.map { |h| MessageHelper.to_postmark(h) }
+ data = serialize(:Messages => mapped)
+
+ with_retries do
+ http_client.post('email/batchWithTemplates', data)
+ end
+ end
+ end
+
def get_stats_totals(options = {})
format_response(http_client.get('stats/outbound', options))
end
View
@@ -37,12 +37,16 @@ def find_each(path, name, options = {})
def with_retries
yield
- rescue DeliveryError
+ rescue HttpServerError, HttpClientError, TimeoutError, Errno::EINVAL,
+ Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError,
+ Net::ProtocolError, SocketError => e
retries = retries ? retries + 1 : 1
- if retries < self.max_retries
+ retriable = !e.respond_to?(:retry?) || e.retry?
+
+ if retriable && retries < self.max_retries
retry
else
- raise
+ raise e
end
end
@@ -52,7 +56,7 @@ def serialize(data)
def take_response_of
[yield, nil]
- rescue DeliveryError => e
+ rescue HttpServerError => e
[e.full_response || {}, e]
end
View
@@ -0,0 +1,117 @@
+module Postmark
+ class Error < ::StandardError; end
+
+ class HttpClientError < Error
+ def retry?
+ true
+ end
+ end
+
+ class HttpServerError < Error
+ attr_accessor :status_code, :parsed_body, :body
+
+ alias_method :full_response, :parsed_body
+
+ def self.build(status_code, body)
+ parsed_body = Postmark::Json.decode(body) rescue {}
+
+ case status_code
+ when '401'
+ InvalidApiKeyError.new(401, body, parsed_body)
+ when '422'
+ ApiInputError.build(body, parsed_body)
+ when '500'
+ InternalServerError.new(500, body, parsed_body)
+ else
+ UnexpectedHttpResponseError.new(status_code, body, parsed_body)
+ end
+ end
+
+ def initialize(status_code = 500, body = '', parsed_body = {})
+ self.parsed_body = parsed_body
+ self.status_code = status_code.to_i
+ message = parsed_body.fetch(
+ 'Message',
+ "The Postmark API responded with HTTP status #{status_code}.")
+
+ super(message)
+ end
+
+ def retry?
+ 5 == status_code / 100
+ end
+ end
+
+ class ApiInputError < HttpServerError
+ INACTIVE_RECIPIENT = 406
+
+ attr_accessor :error_code
+
+ def self.build(body, parsed_body)
+ error_code = parsed_body['ErrorCode'].to_i
+
+ case error_code
+ when INACTIVE_RECIPIENT
+ InactiveRecipientError.new(INACTIVE_RECIPIENT, body, parsed_body)
+ else
+ new(error_code, body, parsed_body)
+ end
+ end
+
+ def initialize(error_code = nil, body = '', parsed_body = {})
+ self.error_code = error_code.to_i
+ super(422, body, parsed_body)
+ end
+
+ def retry?
+ false
+ end
+ end
+
+ class InactiveRecipientError < ApiInputError
+ attr_reader :recipients
+
+ PATTERNS = [/^Found inactive addresses: (.+?)\.$/.freeze,
+ /^Found inactive addresses: (.+?)\.$/.freeze,
+ /these inactive addresses: (.+?)\. Inactive/.freeze].freeze
+
+ def self.parse_recipients(message)
+ PATTERNS.each do |p|
+ _, recipients = p.match(message).to_a
+ next unless recipients
+ return recipients.split(', ')
+ end
+
+ []
+ end
+
+ def initialize(*args)
+ super
+ @recipients = parse_recipients || []
+ end
+
+ private
+
+ def parse_recipients
+ return unless parsed_body && !parsed_body.empty?
+
+ self.class.parse_recipients(parsed_body['Message'])
+ end
+ end
+
+ class TimeoutError < Error
+ def retry?
+ true
+ end
+ end
+
+ class UnknownMessageType < Error; end
+ class InvalidApiKeyError < HttpServerError; end
+ class InternalServerError < HttpServerError; end
+ class UnexpectedHttpResponseError < HttpServerError; end
+
+ # Backwards compatible aliases
+ DeliveryError = Error
+ InvalidMessageError = ApiInputError
+ UnknownError = UnexpectedHttpResponseError
+end
@@ -66,17 +66,10 @@ def url
end
def handle_response(response)
- case response.code.to_i
- when 200
- return Postmark::Json.decode(response.body)
- when 401
- raise error(InvalidApiKeyError, response.body)
- when 422
- raise error(InvalidMessageError, response.body)
- when 500
- raise error(InternalServerError, response.body)
+ if response.code.to_i == 200
+ Postmark::Json.decode(response.body)
else
- raise UnknownError, response
+ raise HttpServerError.build(response.code, response.body)
end
end
@@ -92,8 +85,10 @@ def do_request
@request_mutex.synchronize do
handle_response(yield(http))
end
- rescue Timeout::Error
- raise TimeoutError.new($!)
+ rescue Timeout::Error => e
+ raise TimeoutError.new(e)
+ rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError => e
+ raise HttpClientError.new(e.message)
end
def build_http
@@ -108,18 +103,5 @@ def build_http
http.ssl_version = :TLSv1 if http.respond_to?(:ssl_version=)
http
end
-
- def error_message(response_body)
- Postmark::Json.decode(response_body)["Message"]
- end
-
- def error_message_and_code(response_body)
- reply = Postmark::Json.decode(response_body)
- [reply["Message"], reply["ErrorCode"], reply]
- end
-
- def error(clazz, response_body)
- clazz.send(:new, *error_message_and_code(response_body))
- end
end
end
@@ -31,7 +31,7 @@ def headers_part
'Headers' => @message.export_headers,
'Tag' => @message.tag.to_s,
'TrackOpens' => (cast_to_bool(@message.track_opens) unless @message.track_opens.empty?),
- 'TrackLinks' => (@message.track_links unless @message.track_links.empty?)
+ 'TrackLinks' => (::Postmark::Inflector.to_postmark(@message.track_links) unless @message.track_links.empty?)
}
end
View
@@ -1,3 +1,3 @@
module Postmark
- VERSION = '1.10.0'
+ VERSION = '1.11.0'
end
View
@@ -12,6 +12,7 @@
require 'rspec'
require 'rspec/its'
require File.join(File.expand_path(File.dirname(__FILE__)), 'support', 'shared_examples.rb')
+require File.join(File.expand_path(File.dirname(__FILE__)), 'support', 'custom_matchers.rb')
require File.join(File.expand_path(File.dirname(__FILE__)), 'support', 'helpers.rb')
if ENV['JSONGEM']
Oops, something went wrong.

0 comments on commit 705818e

Please sign in to comment.