postmark 0.9.19 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +8 -0
- data/CHANGELOG.rdoc +20 -0
- data/Gemfile +6 -0
- data/README.md +351 -91
- data/VERSION +1 -1
- data/lib/postmark.rb +40 -132
- data/lib/postmark/api_client.rb +162 -0
- data/lib/postmark/bounce.rb +20 -17
- data/lib/postmark/handlers/mail.rb +10 -3
- data/lib/postmark/helpers/hash_helper.rb +35 -0
- data/lib/postmark/helpers/message_helper.rb +62 -0
- data/lib/postmark/http_client.rb +44 -28
- data/lib/postmark/inbound.rb +21 -0
- data/lib/postmark/inflector.rb +28 -0
- data/lib/postmark/message_extensions/mail.rb +50 -5
- data/lib/postmark/message_extensions/shared.rb +23 -28
- data/lib/postmark/version.rb +1 -1
- data/postmark.gemspec +4 -7
- data/spec/data/empty.gif +0 -0
- data/spec/integration/api_client_hashes_spec.rb +101 -0
- data/spec/integration/api_client_messages_spec.rb +127 -0
- data/spec/integration/mail_delivery_method_spec.rb +80 -0
- data/spec/spec_helper.rb +15 -5
- data/spec/support/helpers.rb +11 -0
- data/spec/{shared_examples.rb → support/shared_examples.rb} +0 -0
- data/spec/unit/postmark/api_client_spec.rb +246 -0
- data/spec/unit/postmark/bounce_spec.rb +142 -0
- data/spec/unit/postmark/handlers/mail_spec.rb +39 -0
- data/spec/unit/postmark/helpers/hash_helper_spec.rb +34 -0
- data/spec/unit/postmark/helpers/message_helper_spec.rb +115 -0
- data/spec/unit/postmark/http_client_spec.rb +204 -0
- data/spec/unit/postmark/inbound_spec.rb +88 -0
- data/spec/unit/postmark/inflector_spec.rb +35 -0
- data/spec/unit/postmark/json_spec.rb +37 -0
- data/spec/unit/postmark/message_extensions/mail_spec.rb +205 -0
- data/spec/unit/postmark_spec.rb +164 -0
- metadata +45 -93
- data/lib/postmark/attachments_fix_for_mail.rb +0 -48
- data/lib/postmark/message_extensions/tmail.rb +0 -115
- data/spec/bounce_spec.rb +0 -53
- data/spec/postmark_spec.rb +0 -253
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
data/lib/postmark.rb
CHANGED
@@ -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
|
-
|
45
|
-
:proxy_host, :proxy_port, :proxy_user, :proxy_pass, :max_retries, :sleep_between_retries
|
49
|
+
@@api_client_mutex = Mutex.new
|
46
50
|
|
47
|
-
|
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
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
156
|
-
|
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
|
168
|
-
|
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
|
178
|
-
|
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
|
data/lib/postmark/bounce.rb
CHANGED
@@ -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
|
-
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
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
|
33
|
+
Postmark.api_client.dump_bounce(id)[:body]
|
33
34
|
end
|
34
35
|
|
35
36
|
def activate
|
36
|
-
Bounce.new(Postmark
|
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
|
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
|
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
|
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
|
-
|
12
|
-
|
13
|
-
|
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
|