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 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
+ ```
@@ -1,14 +1,15 @@
1
1
  require 'net/http'
2
2
  require 'json'
3
3
  require 'cgi'
4
- require 'uri'
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.fetch('MAILGUN_SMTP_LOGIN').split('@').last }
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, request_uri(path, params))
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, path, attributes)
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, path, attributes)
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, path)
244
+ transmit(Net::HTTP::Delete.new(path))
274
245
  end
275
246
 
276
- def transmit(subclass, path, attributes = nil)
277
- message = subclass.new(path)
247
+ def transmit(message)
278
248
  message.basic_auth('api', @api_key)
279
- message.body = URI.encode_www_form(attributes) if attributes
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
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'mailgunner'
3
- s.version = '1.2.0'
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('faux', '~> 1.1.0')
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