mailgunner 1.2.0 → 1.3.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.
- data/README.md +17 -1
- data/lib/mailgunner.rb +22 -87
- data/lib/mailgunner/delivery_method.rb +38 -0
- data/lib/mailgunner/response.rb +37 -0
- data/mailgunner.gemspec +4 -2
- data/spec/mailgunner_delivery_method_spec.rb +57 -0
- data/spec/mailgunner_response_spec.rb +101 -0
- data/spec/mailgunner_spec.rb +108 -347
- metadata +42 -5
data/README.md
CHANGED
@@ -37,7 +37,23 @@ Environment variables
|
|
37
37
|
|
38
38
|
Best practice for storing credentials for external services is to use environment
|
39
39
|
variables, as described by [12factor.net/config](http://www.12factor.net/config).
|
40
|
-
|
41
40
|
Mailgunner::Client defaults to extracting the domain and api_key values it needs
|
42
41
|
from the MAILGUN_API_KEY and MAILGUN_SMTP_LOGIN environment variables. These will
|
43
42
|
exist if you are using Mailgun on Heroku, or you can set them manually.
|
43
|
+
|
44
|
+
|
45
|
+
Email validation
|
46
|
+
----------------
|
47
|
+
|
48
|
+
If you only need [email validation](http://documentation.mailgun.com/api-email-validation.html),
|
49
|
+
you can instead use your Mailgun public key to authenticate like this:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
require 'mailgunner'
|
53
|
+
|
54
|
+
public_key = 'pubkey-5ogiflzbnjrljiky49qxsiozqef5jxp7'
|
55
|
+
|
56
|
+
mailgun = Mailgunner::Client.new(api_key: public_key)
|
57
|
+
|
58
|
+
response = mailgun.validate_address('john@gmail.com')
|
59
|
+
```
|
data/lib/mailgunner.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require 'json'
|
3
3
|
require 'cgi'
|
4
|
-
require '
|
4
|
+
require 'mailgunner/response'
|
5
|
+
require 'mailgunner/delivery_method' if defined?(ActionMailer)
|
5
6
|
|
6
7
|
module Mailgunner
|
7
8
|
class Client
|
8
9
|
attr_accessor :domain, :api_key, :http
|
9
10
|
|
10
11
|
def initialize(options = {})
|
11
|
-
@domain = options.fetch(:domain) { ENV
|
12
|
+
@domain = options.fetch(:domain) { ENV['MAILGUN_SMTP_LOGIN'].to_s.split('@').last }
|
12
13
|
|
13
14
|
@api_key = options.fetch(:api_key) { ENV.fetch('MAILGUN_API_KEY') }
|
14
15
|
|
@@ -25,18 +26,6 @@ module Mailgunner
|
|
25
26
|
@http.use_ssl = true
|
26
27
|
end
|
27
28
|
|
28
|
-
def json
|
29
|
-
Kernel.warn 'Mailgunner::Client#json is deprecated'
|
30
|
-
|
31
|
-
@json
|
32
|
-
end
|
33
|
-
|
34
|
-
def json=(json)
|
35
|
-
Kernel.warn 'Mailgunner::Client#json= is deprecated'
|
36
|
-
|
37
|
-
@json = json
|
38
|
-
end
|
39
|
-
|
40
29
|
def validate_address(value)
|
41
30
|
get('/v2/address/validate', address: value)
|
42
31
|
end
|
@@ -49,6 +38,14 @@ module Mailgunner
|
|
49
38
|
post("/v2/#{escape @domain}/messages", attributes)
|
50
39
|
end
|
51
40
|
|
41
|
+
def send_mime(mail)
|
42
|
+
to = ['to', Array(mail.to).join(',')]
|
43
|
+
|
44
|
+
message = ['message', mail.encoded, {filename: 'message.mime'}]
|
45
|
+
|
46
|
+
multipart_post("/v2/#{escape @domain}/messages.mime", [to, message])
|
47
|
+
end
|
48
|
+
|
52
49
|
def get_domains(params = {})
|
53
50
|
get('/v2/domains', params)
|
54
51
|
end
|
@@ -113,12 +110,6 @@ module Mailgunner
|
|
113
110
|
get("/v2/#{escape @domain}/stats", params)
|
114
111
|
end
|
115
112
|
|
116
|
-
def get_log(params = {})
|
117
|
-
Kernel.warn 'Mailgunner::Client#get_log is deprecated'
|
118
|
-
|
119
|
-
get("/v2/#{escape @domain}/log", params)
|
120
|
-
end
|
121
|
-
|
122
113
|
def get_events(params = {})
|
123
114
|
get("/v2/#{escape @domain}/events", params)
|
124
115
|
end
|
@@ -143,30 +134,6 @@ module Mailgunner
|
|
143
134
|
delete("/v2/routes/#{escape id}")
|
144
135
|
end
|
145
136
|
|
146
|
-
def get_mailboxes(params = {})
|
147
|
-
Kernel.warn 'Mailgunner::Client#get_mailboxes is deprecated'
|
148
|
-
|
149
|
-
get("/v2/#{escape @domain}/mailboxes", params)
|
150
|
-
end
|
151
|
-
|
152
|
-
def add_mailbox(attributes = {})
|
153
|
-
Kernel.warn 'Mailgunner::Client#add_mailbox is deprecated'
|
154
|
-
|
155
|
-
post("/v2/#{escape @domain}/mailboxes", attributes)
|
156
|
-
end
|
157
|
-
|
158
|
-
def update_mailbox(name, attributes = {})
|
159
|
-
Kernel.warn 'Mailgunner::Client#update_mailbox is deprecated'
|
160
|
-
|
161
|
-
put("/v2/#{escape @domain}/mailboxes/#{escape name}", attributes)
|
162
|
-
end
|
163
|
-
|
164
|
-
def delete_mailbox(name)
|
165
|
-
Kernel.warn 'Mailgunner::Client#delete_mailbox is deprecated'
|
166
|
-
|
167
|
-
delete("/v2/#{escape @domain}/mailboxes/#{escape name}")
|
168
|
-
end
|
169
|
-
|
170
137
|
def get_campaigns(params = {})
|
171
138
|
get("/v2/#{escape @domain}/campaigns", params)
|
172
139
|
end
|
@@ -258,25 +225,29 @@ module Mailgunner
|
|
258
225
|
private
|
259
226
|
|
260
227
|
def get(path, params = {})
|
261
|
-
transmit(Net::HTTP::Get
|
228
|
+
transmit(Net::HTTP::Get.new(request_uri(path, params)))
|
262
229
|
end
|
263
230
|
|
264
231
|
def post(path, attributes = {})
|
265
|
-
transmit(Net::HTTP::Post
|
232
|
+
transmit(Net::HTTP::Post.new(path)) { |message| message.set_form_data(attributes) }
|
233
|
+
end
|
234
|
+
|
235
|
+
def multipart_post(path, data)
|
236
|
+
transmit(Net::HTTP::Post.new(path)) { |message| message.set_form(data, 'multipart/form-data') }
|
266
237
|
end
|
267
238
|
|
268
239
|
def put(path, attributes = {})
|
269
|
-
transmit(Net::HTTP::Put
|
240
|
+
transmit(Net::HTTP::Put.new(path)) { |message| message.set_form_data(attributes) }
|
270
241
|
end
|
271
242
|
|
272
243
|
def delete(path)
|
273
|
-
transmit(Net::HTTP::Delete
|
244
|
+
transmit(Net::HTTP::Delete.new(path))
|
274
245
|
end
|
275
246
|
|
276
|
-
def transmit(
|
277
|
-
message = subclass.new(path)
|
247
|
+
def transmit(message)
|
278
248
|
message.basic_auth('api', @api_key)
|
279
|
-
|
249
|
+
|
250
|
+
yield message if block_given?
|
280
251
|
|
281
252
|
Response.new(@http.request(message), :json => @json)
|
282
253
|
end
|
@@ -301,40 +272,4 @@ module Mailgunner
|
|
301
272
|
CGI.escape(component.to_s)
|
302
273
|
end
|
303
274
|
end
|
304
|
-
|
305
|
-
class Response
|
306
|
-
def initialize(http_response, options = {})
|
307
|
-
@http_response = http_response
|
308
|
-
|
309
|
-
@json = options.fetch(:json) { JSON }
|
310
|
-
end
|
311
|
-
|
312
|
-
def method_missing(name, *args, &block)
|
313
|
-
@http_response.send(name, *args, &block)
|
314
|
-
end
|
315
|
-
|
316
|
-
def respond_to_missing?(name, include_private = false)
|
317
|
-
@http_response.respond_to?(name)
|
318
|
-
end
|
319
|
-
|
320
|
-
def ok?
|
321
|
-
code.to_i == 200
|
322
|
-
end
|
323
|
-
|
324
|
-
def client_error?
|
325
|
-
(400 .. 499).include?(code.to_i)
|
326
|
-
end
|
327
|
-
|
328
|
-
def server_error?
|
329
|
-
(500 .. 599).include?(code.to_i)
|
330
|
-
end
|
331
|
-
|
332
|
-
def json?
|
333
|
-
self['Content-Type'].split(';').first == 'application/json'
|
334
|
-
end
|
335
|
-
|
336
|
-
def object
|
337
|
-
@object ||= @json.parse(body)
|
338
|
-
end
|
339
|
-
end
|
340
275
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'mail/check_delivery_params'
|
2
|
+
|
3
|
+
module Mailgunner
|
4
|
+
class DeliveryFailed < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class DeliveryMethod
|
8
|
+
include Mail::CheckDeliveryParams
|
9
|
+
|
10
|
+
attr_accessor :settings
|
11
|
+
|
12
|
+
def initialize(values)
|
13
|
+
@settings = values
|
14
|
+
|
15
|
+
@client = if @settings.has_key?(:domain)
|
16
|
+
Client.new(domain: @settings[:domain])
|
17
|
+
else
|
18
|
+
Client.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def deliver!(mail)
|
23
|
+
check_delivery_params(mail)
|
24
|
+
|
25
|
+
response = @client.send_mime(mail)
|
26
|
+
|
27
|
+
if response.ok?
|
28
|
+
return response
|
29
|
+
elsif response.json? && response.object.has_key?('message')
|
30
|
+
raise DeliveryFailed, response.object['message']
|
31
|
+
else
|
32
|
+
raise DeliveryFailed, "#{response.code} #{response.message}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
ActionMailer::Base.add_delivery_method :mailgun, DeliveryMethod
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Mailgunner
|
2
|
+
class Response
|
3
|
+
def initialize(http_response, options = {})
|
4
|
+
@http_response = http_response
|
5
|
+
|
6
|
+
@json = options.fetch(:json) { JSON }
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(name, *args, &block)
|
10
|
+
@http_response.send(name, *args, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def respond_to_missing?(name, include_private = false)
|
14
|
+
@http_response.respond_to?(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def ok?
|
18
|
+
code.to_i == 200
|
19
|
+
end
|
20
|
+
|
21
|
+
def client_error?
|
22
|
+
(400 .. 499).include?(code.to_i)
|
23
|
+
end
|
24
|
+
|
25
|
+
def server_error?
|
26
|
+
(500 .. 599).include?(code.to_i)
|
27
|
+
end
|
28
|
+
|
29
|
+
def json?
|
30
|
+
self['Content-Type'].split(';').first == 'application/json'
|
31
|
+
end
|
32
|
+
|
33
|
+
def object
|
34
|
+
@object ||= @json.parse(body)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/mailgunner.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'mailgunner'
|
3
|
-
s.version = '1.
|
3
|
+
s.version = '1.3.0'
|
4
4
|
s.platform = Gem::Platform::RUBY
|
5
5
|
s.authors = ['Tim Craft']
|
6
6
|
s.email = ['mail@timcraft.com']
|
@@ -10,6 +10,8 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.files = Dir.glob('{lib,spec}/**/*') + %w(README.md mailgunner.gemspec)
|
11
11
|
s.add_development_dependency('rake', '>= 0.9.3')
|
12
12
|
s.add_development_dependency('mocha', '~> 0.13.2')
|
13
|
-
s.add_development_dependency('
|
13
|
+
s.add_development_dependency('webmock', '~> 1.13.0')
|
14
|
+
s.add_development_dependency('mail', '~> 2.5.4')
|
15
|
+
s.add_development_dependency('actionmailer', '~> 4.0.0')
|
14
16
|
s.require_path = 'lib'
|
15
17
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'webmock/minitest'
|
3
|
+
require 'action_mailer'
|
4
|
+
require 'mailgunner'
|
5
|
+
require 'mailgunner/delivery_method'
|
6
|
+
|
7
|
+
class ExampleMailer < ActionMailer::Base
|
8
|
+
default from: 'testing@localhost'
|
9
|
+
|
10
|
+
def registration_confirmation(user)
|
11
|
+
mail to: user[:email], subject: 'Welcome!', body: 'Hello!'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'Mailgunner::DeliveryMethod' do
|
16
|
+
before do
|
17
|
+
@api_key = 'xxx'
|
18
|
+
|
19
|
+
@domain = 'samples.mailgun.org'
|
20
|
+
|
21
|
+
@base_url = "https://api:#@api_key@api.mailgun.net/v2"
|
22
|
+
|
23
|
+
@address = 'user@example.com'
|
24
|
+
|
25
|
+
ActionMailer::Base.delivery_method = :mailgun
|
26
|
+
|
27
|
+
ENV['MAILGUN_API_KEY'] = @api_key
|
28
|
+
|
29
|
+
ENV['MAILGUN_SMTP_LOGIN'] = "postmaster@#@domain"
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'delivers the mail to mailgun in mime format' do
|
33
|
+
stub_request(:post, "#@base_url/#@domain/messages.mime")
|
34
|
+
|
35
|
+
ExampleMailer.registration_confirmation(email: @address).deliver
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'raises an exception if the api returns an error' do
|
39
|
+
stub_request(:post, "#@base_url/#@domain/messages.mime").to_return({
|
40
|
+
status: 403,
|
41
|
+
headers: {'Content-Type' => 'application/json'},
|
42
|
+
body: '{"message": "Invalid API key"}'
|
43
|
+
})
|
44
|
+
|
45
|
+
proc { ExampleMailer.registration_confirmation(email: @address).deliver }.must_raise(Mailgunner::DeliveryFailed)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'allows the domain to be specified explicitly via the delivery method settings' do
|
49
|
+
stub_request(:post, "#@base_url/app123.mailgun.org/messages.mime")
|
50
|
+
|
51
|
+
ActionMailer::Base.mailgun_settings = {domain: 'app123.mailgun.org'}
|
52
|
+
|
53
|
+
ExampleMailer.registration_confirmation(email: @address).deliver
|
54
|
+
|
55
|
+
ActionMailer::Base.mailgun_settings = {}
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'mocha/setup'
|
3
|
+
require 'mailgunner'
|
4
|
+
|
5
|
+
describe 'Mailgunner::Response' do
|
6
|
+
before do
|
7
|
+
@http_response = mock()
|
8
|
+
|
9
|
+
@response = Mailgunner::Response.new(@http_response)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'delegates missing methods to the http response object' do
|
13
|
+
@http_response.stubs(:code).returns('200')
|
14
|
+
|
15
|
+
@response.code.must_equal('200')
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'ok query method' do
|
19
|
+
it 'returns true if the status code is 200' do
|
20
|
+
@http_response.expects(:code).returns('200')
|
21
|
+
|
22
|
+
@response.ok?.must_equal(true)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns false otherwise' do
|
26
|
+
@http_response.expects(:code).returns('400')
|
27
|
+
|
28
|
+
@response.ok?.must_equal(false)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'client_error query method' do
|
33
|
+
it 'returns true if the status code is 4xx' do
|
34
|
+
@http_response.stubs(:code).returns(%w(400 401 402 404).sample)
|
35
|
+
|
36
|
+
@response.client_error?.must_equal(true)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'returns false otherwise' do
|
40
|
+
@http_response.stubs(:code).returns('200')
|
41
|
+
|
42
|
+
@response.client_error?.must_equal(false)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'server_error query method' do
|
47
|
+
it 'returns true if the status code is 5xx' do
|
48
|
+
@http_response.stubs(:code).returns(%w(500 502 503 504).sample)
|
49
|
+
|
50
|
+
@response.server_error?.must_equal(true)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'returns false otherwise' do
|
54
|
+
@http_response.stubs(:code).returns('200')
|
55
|
+
|
56
|
+
@response.server_error?.must_equal(false)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'json query method' do
|
61
|
+
it 'returns true if the response has a json content type' do
|
62
|
+
@http_response.expects(:[]).with('Content-Type').returns('application/json;charset=utf-8')
|
63
|
+
|
64
|
+
@response.json?.must_equal(true)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'returns false otherwise' do
|
68
|
+
@http_response.expects(:[]).with('Content-Type').returns('text/html')
|
69
|
+
|
70
|
+
@response.json?.must_equal(false)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'object method' do
|
75
|
+
it 'decodes the response body as json and returns a hash' do
|
76
|
+
@http_response.expects(:body).returns('{"foo":"bar"}')
|
77
|
+
|
78
|
+
@response.object.must_equal({'foo' => 'bar'})
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe 'Mailgunner::Response initialized with an alternative json implementation' do
|
84
|
+
before do
|
85
|
+
@json = mock()
|
86
|
+
|
87
|
+
@http_response = stub
|
88
|
+
|
89
|
+
@response = Mailgunner::Response.new(@http_response, :json => @json)
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'object method' do
|
93
|
+
it 'uses the alternative json implementation to parse the response body' do
|
94
|
+
@http_response.stubs(:body).returns(response_body = '{"foo":"bar"}')
|
95
|
+
|
96
|
+
@json.expects(:parse).with(response_body)
|
97
|
+
|
98
|
+
@response.object
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|