postmark 0.9.19 → 1.0.0

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 (41) hide show
  1. data/.travis.yml +8 -0
  2. data/CHANGELOG.rdoc +20 -0
  3. data/Gemfile +6 -0
  4. data/README.md +351 -91
  5. data/VERSION +1 -1
  6. data/lib/postmark.rb +40 -132
  7. data/lib/postmark/api_client.rb +162 -0
  8. data/lib/postmark/bounce.rb +20 -17
  9. data/lib/postmark/handlers/mail.rb +10 -3
  10. data/lib/postmark/helpers/hash_helper.rb +35 -0
  11. data/lib/postmark/helpers/message_helper.rb +62 -0
  12. data/lib/postmark/http_client.rb +44 -28
  13. data/lib/postmark/inbound.rb +21 -0
  14. data/lib/postmark/inflector.rb +28 -0
  15. data/lib/postmark/message_extensions/mail.rb +50 -5
  16. data/lib/postmark/message_extensions/shared.rb +23 -28
  17. data/lib/postmark/version.rb +1 -1
  18. data/postmark.gemspec +4 -7
  19. data/spec/data/empty.gif +0 -0
  20. data/spec/integration/api_client_hashes_spec.rb +101 -0
  21. data/spec/integration/api_client_messages_spec.rb +127 -0
  22. data/spec/integration/mail_delivery_method_spec.rb +80 -0
  23. data/spec/spec_helper.rb +15 -5
  24. data/spec/support/helpers.rb +11 -0
  25. data/spec/{shared_examples.rb → support/shared_examples.rb} +0 -0
  26. data/spec/unit/postmark/api_client_spec.rb +246 -0
  27. data/spec/unit/postmark/bounce_spec.rb +142 -0
  28. data/spec/unit/postmark/handlers/mail_spec.rb +39 -0
  29. data/spec/unit/postmark/helpers/hash_helper_spec.rb +34 -0
  30. data/spec/unit/postmark/helpers/message_helper_spec.rb +115 -0
  31. data/spec/unit/postmark/http_client_spec.rb +204 -0
  32. data/spec/unit/postmark/inbound_spec.rb +88 -0
  33. data/spec/unit/postmark/inflector_spec.rb +35 -0
  34. data/spec/unit/postmark/json_spec.rb +37 -0
  35. data/spec/unit/postmark/message_extensions/mail_spec.rb +205 -0
  36. data/spec/unit/postmark_spec.rb +164 -0
  37. metadata +45 -93
  38. data/lib/postmark/attachments_fix_for_mail.rb +0 -48
  39. data/lib/postmark/message_extensions/tmail.rb +0 -115
  40. data/spec/bounce_spec.rb +0 -53
  41. data/spec/postmark_spec.rb +0 -253
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.19
1
+ 1.0.0
@@ -1,23 +1,28 @@
1
1
  require 'net/http'
2
2
  require 'net/https'
3
+ require 'thread' unless defined? Mutex # For Ruby 1.8.7
3
4
 
5
+ require 'postmark/inflector'
6
+ require 'postmark/helpers/hash_helper'
7
+ require 'postmark/helpers/message_helper'
4
8
  require 'postmark/bounce'
9
+ require 'postmark/inbound'
5
10
  require 'postmark/json'
6
11
  require 'postmark/http_client'
12
+ require 'postmark/api_client'
7
13
  require 'postmark/message_extensions/shared'
8
- require 'postmark/message_extensions/tmail'
9
14
  require 'postmark/message_extensions/mail'
10
15
  require 'postmark/handlers/mail'
11
- require 'postmark/attachments_fix_for_mail'
12
16
 
13
17
  module Postmark
14
18
 
15
19
  class DeliveryError < StandardError
16
- attr_accessor :error_code
20
+ attr_accessor :error_code, :full_response
17
21
 
18
- def initialize(message = nil, error_code = nil)
22
+ def initialize(message = nil, error_code = nil, full_response = nil)
19
23
  super(message)
20
24
  self.error_code = error_code
25
+ self.full_response = full_response
21
26
  end
22
27
  end
23
28
 
@@ -41,151 +46,54 @@ module Postmark
41
46
 
42
47
  extend self
43
48
 
44
- attr_accessor :host, :path_prefix, :port, :secure, :api_key, :http_open_timeout, :http_read_timeout,
45
- :proxy_host, :proxy_port, :proxy_user, :proxy_pass, :max_retries, :sleep_between_retries
49
+ @@api_client_mutex = Mutex.new
46
50
 
47
- attr_writer :response_parser_class
51
+ attr_accessor :secure, :api_key, :proxy_host, :proxy_port, :proxy_user,
52
+ :proxy_pass, :host, :port, :path_prefix,
53
+ :http_open_timeout, :http_read_timeout, :max_retries
54
+
55
+ attr_writer :response_parser_class, :api_client
48
56
 
49
57
  def response_parser_class
50
58
  @response_parser_class ||= defined?(ActiveSupport::JSON) ? :ActiveSupport : :Json
51
59
  end
52
60
 
53
- # The port on which your Postmark server runs.
54
- def port
55
- @port || (secure ? 443 : 80)
56
- end
57
-
58
- # The host to connect to.
59
- def host
60
- @host ||= 'api.postmarkapp.com'
61
- end
62
-
63
- # The path of the listener
64
- def path_prefix
65
- @path_prefix ||= '/'
66
- end
67
-
68
- def http_open_timeout
69
- @http_open_timeout ||= 5
70
- end
71
-
72
- def http_read_timeout
73
- @http_read_timeout ||= 15
74
- end
75
-
76
- def max_retries
77
- @max_retries ||= 3
78
- end
79
-
80
- def sleep_between_retries
81
- @sleep_between_retries ||= 10
82
- end
83
-
84
61
  def configure
85
62
  yield self
86
63
  end
87
64
 
88
- def send_through_postmark(message) #:nodoc:
89
- with_retries do
90
- HttpClient.post("email", Postmark::Json.encode(convert_message_to_options_hash(message)))
91
- end
92
- rescue Timeout::Error
93
- raise TimeoutError.new($!)
94
- end
95
-
96
- def convert_message_to_options_hash(message)
97
- options = Hash.new
98
- headers = extract_headers_according_to_message_format(message)
99
- attachments = message.export_attachments
100
-
101
- options["From"] = message['from'].to_s if message.from
102
- options["ReplyTo"] = Array[message.reply_to].flatten.join(", ") if message.reply_to
103
- options["To"] = message['to'].to_s if message.to
104
- options["Cc"] = message['cc'].to_s if message.cc
105
- options["Bcc"] = Array[message.bcc].flatten.join(", ") if message.bcc
106
- options["Subject"] = message.subject
107
- options["Attachments"] = attachments unless attachments.empty?
108
- options["Tag"] = message.tag.to_s if message.tag
109
- options["Headers"] = headers if headers.size > 0
110
-
111
- options = options.delete_if { |k,v| v.nil? }
112
-
113
- html = message.body_html
114
- text = message.body_text
115
-
116
- if message.multipart?
117
- options["HtmlBody"] = html
118
- options["TextBody"] = text
119
- elsif html
120
- options["HtmlBody"] = html
121
- else
122
- options["TextBody"] = text
123
- end
124
-
125
- options
126
- end
127
-
128
- def delivery_stats
129
- HttpClient.get("deliverystats")
130
- end
131
-
132
- protected
133
-
134
- def with_retries
135
- yield
136
- rescue DeliveryError, Timeout::Error
137
- retries = retries ? retries + 1 : 0
138
- if retries < max_retries
139
- retry
140
- else
141
- raise
142
- end
143
- end
144
-
145
- def extract_headers_according_to_message_format(message)
146
- if defined?(TMail) && message.is_a?(TMail::Mail)
147
- headers = extract_tmail_headers(message)
148
- elsif defined?(Mail) && message.kind_of?(Mail::Message)
149
- headers = extract_mail_headers(message)
150
- else
151
- raise "Can't convert message to a valid hash of API options. Unknown message format."
65
+ def api_client
66
+ return @api_client if @api_client
67
+
68
+ @@api_client_mutex.synchronize do
69
+ @api_client ||= Postmark::ApiClient.new(
70
+ self.api_key,
71
+ :secure => self.secure,
72
+ :proxy_host => self.proxy_host,
73
+ :proxy_port => self.proxy_port,
74
+ :proxy_user => self.proxy_user,
75
+ :proxy_pass => self.proxy_pass,
76
+ :host => self.host,
77
+ :port => self.port,
78
+ :path_prefix => self.path_prefix,
79
+ :max_retries => self.max_retries
80
+ )
152
81
  end
153
82
  end
154
83
 
155
- def extract_mail_headers(message)
156
- headers = []
157
- message.header.fields.each do |field|
158
- key = field.name
159
- value = field.value
160
- next if bogus_headers.include? key.downcase
161
- name = key.split(/-/).map {|i| i.capitalize }.join('-')
162
- headers << { "Name" => name, "Value" => value }
163
- end
164
- headers
84
+ def deliver_message(*args)
85
+ api_client.deliver_message(*args)
165
86
  end
87
+ alias_method :send_through_postmark, :deliver_message
166
88
 
167
- def extract_tmail_headers(message)
168
- headers = []
169
- message.each_header do |key, value|
170
- next if bogus_headers.include? key.downcase
171
- name = key.split(/-/).map {|i| i.capitalize }.join('-')
172
- headers << { "Name" => name, "Value" => value.body }
173
- end
174
- headers
89
+ def deliver_messages(*args)
90
+ api_client.deliver_messages(*args)
175
91
  end
176
92
 
177
- def bogus_headers
178
- %q[
179
- return-path x-pm-rcpt
180
- from reply-to
181
- sender received
182
- date content-type
183
- cc bcc
184
- subject tag
185
- attachment
186
- ]
93
+ def delivery_stats(*args)
94
+ api_client.delivery_stats(*args)
187
95
  end
188
96
 
189
- self.response_parser_class = nil
190
-
191
97
  end
98
+
99
+ Postmark.response_parser_class = nil
@@ -0,0 +1,162 @@
1
+ module Postmark
2
+ class ApiClient
3
+ attr_reader :http_client, :max_retries
4
+ attr_writer :max_batch_size
5
+
6
+ def initialize(api_key, options = {})
7
+ @max_retries = options.delete(:max_retries) || 3
8
+ @http_client = HttpClient.new(api_key, options)
9
+ end
10
+
11
+ def api_key=(api_key)
12
+ http_client.api_key = api_key
13
+ end
14
+
15
+ def deliver(message_hash = {})
16
+ data = serialize(MessageHelper.to_postmark(message_hash))
17
+
18
+ with_retries do
19
+ format_response http_client.post("email", data)
20
+ end
21
+ end
22
+
23
+ def deliver_in_batches(message_hashes)
24
+ in_batches(message_hashes) do |batch, offset|
25
+ data = serialize(batch.map { |h| MessageHelper.to_postmark(h) })
26
+
27
+ with_retries do
28
+ http_client.post("email/batch", data)
29
+ end
30
+ end
31
+ end
32
+
33
+ def deliver_message(message)
34
+ data = serialize(message.to_postmark_hash)
35
+
36
+ with_retries do
37
+ take_response_of { http_client.post("email", data) }.to do |response|
38
+ update_message(message, response)
39
+ format_response response, true
40
+ end
41
+ end
42
+ end
43
+
44
+ def deliver_messages(messages)
45
+ in_batches(messages) do |batch, offset|
46
+ data = serialize(batch.map { |m| m.to_postmark_hash })
47
+
48
+ with_retries do
49
+ http_client.post("email/batch", data).tap do |response|
50
+ response.each_with_index do |r, i|
51
+ update_message(messages[offset + i], r)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def delivery_stats
59
+ response = format_response(http_client.get("deliverystats"), true)
60
+
61
+ if response[:bounces]
62
+ response[:bounces] = format_response(response[:bounces])
63
+ end
64
+
65
+ response
66
+ end
67
+
68
+ def get_bounces(options = {})
69
+ format_response http_client.get("bounces", options)["Bounces"]
70
+ end
71
+
72
+ def get_bounced_tags
73
+ http_client.get("bounces/tags")
74
+ end
75
+
76
+ def get_bounce(id)
77
+ format_response http_client.get("bounces/#{id}")
78
+ end
79
+
80
+ def dump_bounce(id)
81
+ format_response http_client.get("bounces/#{id}/dump")
82
+ end
83
+
84
+ def activate_bounce(id)
85
+ format_response http_client.put("bounces/#{id}/activate")["Bounce"]
86
+ end
87
+
88
+ def server_info
89
+ format_response http_client.get("server")
90
+ end
91
+
92
+ def update_server_info(attributes = {})
93
+ data = HashHelper.to_postmark(attributes)
94
+ format_response http_client.post("server", serialize(data))
95
+ end
96
+
97
+ def max_batch_size
98
+ @max_batch_size ||= 500
99
+ end
100
+
101
+ protected
102
+
103
+ def with_retries
104
+ yield
105
+ rescue DeliveryError
106
+ retries = retries ? retries + 1 : 1
107
+ if retries < self.max_retries
108
+ retry
109
+ else
110
+ raise
111
+ end
112
+ end
113
+
114
+ def in_batches(messages)
115
+ r = messages.each_slice(max_batch_size).each_with_index.map do |batch, i|
116
+ yield batch, i * max_batch_size
117
+ end
118
+
119
+ format_response r.flatten
120
+ end
121
+
122
+ def update_message(message, response)
123
+ response ||= {}
124
+ message['Message-ID'] = response['MessageID']
125
+ message.delivered = !!response['MessageID']
126
+ message.postmark_response = response
127
+ end
128
+
129
+ def serialize(data)
130
+ Postmark::Json.encode(data)
131
+ end
132
+
133
+ def take_response_of
134
+ define_singleton_method(:to, yield)
135
+ rescue DeliveryError => e
136
+ define_singleton_method(:to, e.full_response || {}) do
137
+ raise e
138
+ end
139
+ end
140
+
141
+ def define_singleton_method(name, object)
142
+ singleton_class = class << object; self; end
143
+ singleton_class.send(:define_method, name) do |&b|
144
+ ret = b.call(self) if b
145
+ yield if block_given?
146
+ ret
147
+ end
148
+ object
149
+ end
150
+
151
+ def format_response(response, compatible = false)
152
+ return {} unless response
153
+
154
+ if response.kind_of? Array
155
+ response.map { |entry| Postmark::HashHelper.to_ruby(entry, compatible) }
156
+ else
157
+ Postmark::HashHelper.to_ruby(response, compatible)
158
+ end
159
+ end
160
+
161
+ end
162
+ end
@@ -6,18 +6,19 @@ module Postmark
6
6
  attr_reader :email, :bounced_at, :type, :details, :name, :id, :server_id, :tag, :message_id, :subject
7
7
 
8
8
  def initialize(values = {})
9
- @id = values["ID"]
10
- @email = values["Email"]
11
- @bounced_at = Time.parse(values["BouncedAt"])
12
- @type = values["Type"]
13
- @name = values["Name"]
14
- @details = values["Details"]
15
- @tag = values["Tag"]
16
- @dump_available = values["DumpAvailable"]
17
- @inactive = values["Inactive"]
18
- @can_activate = values["CanActivate"]
19
- @message_id = values["MessageID"]
20
- @subject = values["Subject"]
9
+ values = Postmark::HashHelper.to_ruby(values)
10
+ @id = values[:id]
11
+ @email = values[:email]
12
+ @bounced_at = Time.parse(values[:bounced_at])
13
+ @type = values[:type]
14
+ @name = values[:name]
15
+ @details = values[:details]
16
+ @tag = values[:tag]
17
+ @dump_available = values[:dump_available]
18
+ @inactive = values[:inactive]
19
+ @can_activate = values[:can_activate]
20
+ @message_id = values[:message_id]
21
+ @subject = values[:subject]
21
22
  end
22
23
 
23
24
  def inactive?
@@ -29,11 +30,11 @@ module Postmark
29
30
  end
30
31
 
31
32
  def dump
32
- Postmark::HttpClient.get("bounces/#{id}/dump")["Body"]
33
+ Postmark.api_client.dump_bounce(id)[:body]
33
34
  end
34
35
 
35
36
  def activate
36
- Bounce.new(Postmark::HttpClient.put("bounces/#{id}/activate")["Bounce"])
37
+ Bounce.new(Postmark.api_client.activate_bounce(id))
37
38
  end
38
39
 
39
40
  def dump_available?
@@ -42,17 +43,19 @@ module Postmark
42
43
 
43
44
  class << self
44
45
  def find(id)
45
- Bounce.new(Postmark::HttpClient.get("bounces/#{id}"))
46
+ Bounce.new(Postmark.api_client.get_bounce(id))
46
47
  end
47
48
 
48
49
  def all(options = {})
49
50
  options[:count] ||= 30
50
51
  options[:offset] ||= 0
51
- Postmark::HttpClient.get("bounces", options)['Bounces'].map { |bounce_json| Bounce.new(bounce_json) }
52
+ Postmark.api_client.get_bounces(options).map do |bounce_json|
53
+ Bounce.new(bounce_json)
54
+ end
52
55
  end
53
56
 
54
57
  def tags
55
- Postmark::HttpClient.get("bounces/tags")
58
+ Postmark.api_client.get_bounced_tags
56
59
  end
57
60
  end
58
61
 
@@ -8,9 +8,16 @@ module Mail
8
8
  end
9
9
 
10
10
  def deliver!(mail)
11
- ::Postmark.api_key = settings[:api_key]
12
- response = ::Postmark.send_through_postmark(mail)
13
- mail["Message-ID"] = response["MessageID"] if response.kind_of?(Hash)
11
+ settings = self.settings.dup
12
+ api_key = settings.delete(:api_key)
13
+ api_client = ::Postmark::ApiClient.new(api_key, settings)
14
+ response = api_client.deliver_message(mail)
15
+
16
+ if settings[:return_response]
17
+ response
18
+ else
19
+ self
20
+ end
14
21
  end
15
22
 
16
23
  end