postmark 0.9.19 → 1.0.0

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