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