mailgun-ruby 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +8 -0
  4. data/.rubocop_todo.yml +22 -0
  5. data/.ruby-env.yml.example +12 -0
  6. data/.travis.yml +6 -12
  7. data/Domains.md +36 -0
  8. data/MessageBuilder.md +14 -14
  9. data/Messages.md +44 -30
  10. data/OptInHandler.md +34 -34
  11. data/README.md +74 -24
  12. data/Rakefile +22 -20
  13. data/Snippets.md +26 -26
  14. data/Webhooks.md +40 -0
  15. data/lib/mailgun.rb +26 -228
  16. data/lib/mailgun/chains.rb +16 -0
  17. data/lib/mailgun/client.rb +143 -0
  18. data/lib/mailgun/domains/domains.rb +84 -0
  19. data/lib/mailgun/events/events.rb +53 -35
  20. data/lib/mailgun/exceptions/exceptions.rb +43 -10
  21. data/lib/mailgun/lists/opt_in_handler.rb +18 -19
  22. data/lib/mailgun/messages/batch_message.rb +31 -48
  23. data/lib/mailgun/messages/message_builder.rb +160 -144
  24. data/lib/mailgun/response.rb +55 -0
  25. data/lib/mailgun/version.rb +2 -3
  26. data/lib/mailgun/webhooks/webhooks.rb +101 -0
  27. data/mailgun.gemspec +16 -10
  28. data/spec/integration/bounces_spec.rb +44 -0
  29. data/spec/integration/campaign_spec.rb +60 -0
  30. data/spec/integration/complaints_spec.rb +38 -0
  31. data/spec/integration/domains_spec.rb +39 -0
  32. data/spec/integration/email_validation_spec.rb +29 -0
  33. data/spec/integration/events_spec.rb +20 -0
  34. data/spec/integration/list_members_spec.rb +63 -0
  35. data/spec/integration/list_spec.rb +58 -0
  36. data/spec/integration/mailgun_spec.rb +26 -550
  37. data/spec/integration/routes_spec.rb +74 -0
  38. data/spec/integration/stats_spec.rb +15 -0
  39. data/spec/integration/unsubscribes_spec.rb +42 -0
  40. data/spec/integration/webhook_spec.rb +54 -0
  41. data/spec/spec_helper.rb +37 -7
  42. data/spec/unit/connection/test_client.rb +15 -95
  43. data/spec/unit/events/events_spec.rb +9 -6
  44. data/spec/unit/lists/opt_in_handler_spec.rb +6 -4
  45. data/spec/unit/mailgun_spec.rb +25 -19
  46. data/spec/unit/messages/batch_message_spec.rb +47 -38
  47. data/spec/unit/messages/message_builder_spec.rb +282 -111
  48. data/vcr_cassettes/bounces.yml +175 -0
  49. data/vcr_cassettes/complaints.yml +175 -0
  50. data/vcr_cassettes/domains.todo.yml +42 -0
  51. data/vcr_cassettes/domains.yml +360 -0
  52. data/vcr_cassettes/email_validation.yml +104 -0
  53. data/vcr_cassettes/events.yml +61 -0
  54. data/vcr_cassettes/list_members.yml +320 -0
  55. data/vcr_cassettes/mailing_list.todo.yml +43 -0
  56. data/vcr_cassettes/mailing_list.yml +390 -0
  57. data/vcr_cassettes/routes.yml +359 -0
  58. data/vcr_cassettes/send_message.yml +107 -0
  59. data/vcr_cassettes/stats.yml +44 -0
  60. data/vcr_cassettes/unsubscribes.yml +191 -0
  61. data/vcr_cassettes/webhooks.yml +276 -0
  62. metadata +114 -10
@@ -0,0 +1,16 @@
1
+ module Mailgun
2
+
3
+ # Public constants used throughout
4
+ class Chains
5
+
6
+ # maximum campaign ids per message
7
+ MAX_CAMPAIGN_IDS = 3
8
+
9
+ # maximum tags per message
10
+ MAX_TAGS = 3
11
+
12
+ # maximum recipients per message or batch
13
+ MAX_RECIPIENTS = 1000
14
+
15
+ end
16
+ end
@@ -0,0 +1,143 @@
1
+ require 'mailgun/chains'
2
+ require 'mailgun/exceptions/exceptions'
3
+
4
+ module Mailgun
5
+ # A Mailgun::Client object is used to communicate with the Mailgun API. It is a
6
+ # wrapper around RestClient so you don't have to worry about the HTTP aspect
7
+ # of communicating with our API.
8
+ #
9
+ # See the Github documentation for full examples.
10
+ class Client
11
+
12
+ def initialize(api_key = Mailgun.api_key,
13
+ api_host = 'api.mailgun.net',
14
+ api_version = 'v3',
15
+ ssl = true)
16
+
17
+ endpoint = endpoint_generator(api_host, api_version, ssl)
18
+ @http_client = RestClient::Resource.new(endpoint,
19
+ user: 'api',
20
+ password: api_key,
21
+ user_agent: "mailgun-sdk-ruby/#{Mailgun::VERSION}")
22
+ end
23
+
24
+ # Simple Message Sending
25
+ #
26
+ # @param [String] working_domain This is the domain you wish to send from.
27
+ # @param [Hash] data This should be a standard Hash
28
+ # containing required parameters for the requested resource.
29
+ # @return [Mailgun::Response] A Mailgun::Response object.
30
+ def send_message(working_domain, data)
31
+ case data
32
+ when Hash
33
+ if data.key?(:message)
34
+ if data[:message].is_a?(String)
35
+ data[:message] = convert_string_to_file(data[:message])
36
+ end
37
+ return post("#{working_domain}/messages.mime", data)
38
+ end
39
+ post("#{working_domain}/messages", data)
40
+ when MessageBuilder
41
+ post("#{working_domain}/messages", data.message)
42
+ else
43
+ fail ParameterError.new('Unknown data type for data parameter.', data)
44
+ end
45
+ end
46
+
47
+ # Generic Mailgun POST Handler
48
+ #
49
+ # @param [String] resource_path This is the API resource you wish to interact
50
+ # with. Be sure to include your domain, where necessary.
51
+ # @param [Hash] data This should be a standard Hash
52
+ # containing required parameters for the requested resource.
53
+ # @return [Mailgun::Response] A Mailgun::Response object.
54
+ def post(resource_path, data)
55
+ response = @http_client[resource_path].post(data)
56
+ Response.new(response)
57
+ rescue => err
58
+ raise communication_error err
59
+ end
60
+
61
+ # Generic Mailgun GET Handler
62
+ #
63
+ # @param [String] resource_path This is the API resource you wish to interact
64
+ # with. Be sure to include your domain, where necessary.
65
+ # @param [Hash] query_string This should be a standard Hash
66
+ # containing required parameters for the requested resource.
67
+ # @return [Mailgun::Response] A Mailgun::Response object.
68
+ def get(resource_path, params = nil, accept = '*/*')
69
+ if params
70
+ response = @http_client[resource_path].get(params: params, accept: accept)
71
+ else
72
+ response = @http_client[resource_path].get(accept: accept)
73
+ end
74
+ Response.new(response)
75
+ rescue => err
76
+ raise communication_error err
77
+ end
78
+
79
+ # Generic Mailgun PUT Handler
80
+ #
81
+ # @param [String] resource_path This is the API resource you wish to interact
82
+ # with. Be sure to include your domain, where necessary.
83
+ # @param [Hash] data This should be a standard Hash
84
+ # containing required parameters for the requested resource.
85
+ # @return [Mailgun::Response] A Mailgun::Response object.
86
+ def put(resource_path, data)
87
+ response = @http_client[resource_path].put(data)
88
+ Response.new(response)
89
+ rescue => err
90
+ raise communication_error err
91
+ end
92
+
93
+ # Generic Mailgun DELETE Handler
94
+ #
95
+ # @param [String] resource_path This is the API resource you wish to interact
96
+ # with. Be sure to include your domain, where necessary.
97
+ # @return [Mailgun::Response] A Mailgun::Response object.
98
+ def delete(resource_path)
99
+ response = @http_client[resource_path].delete
100
+ Response.new(response)
101
+ rescue => err
102
+ raise communication_error err
103
+ end
104
+
105
+ private
106
+
107
+ # Converts MIME string to file for easy uploading to API
108
+ #
109
+ # @param [String] string MIME string to post to API
110
+ # @return [File] File object
111
+ def convert_string_to_file(string)
112
+ file = Tempfile.new('MG_TMP_MIME')
113
+ file.write(string)
114
+ file.rewind
115
+ file
116
+ end
117
+
118
+ # Generates the endpoint URL to for the API. Allows overriding
119
+ # API endpoint, API versions, and toggling SSL.
120
+ #
121
+ # @param [String] api_host URL endpoint the library will hit
122
+ # @param [String] api_version The version of the API to hit
123
+ # @param [Boolean] ssl True, SSL. False, No SSL.
124
+ # @return [string] concatenated URL string
125
+ def endpoint_generator(api_host, api_version, ssl)
126
+ ssl ? scheme = 'https' : scheme = 'http'
127
+ if api_version
128
+ "#{scheme}://#{api_host}/#{api_version}"
129
+ else
130
+ "#{scheme}://#{api_host}"
131
+ end
132
+ end
133
+
134
+ # Raises CommunicationError and stores response in it if present
135
+ #
136
+ # @param [StandardException] e upstream exception object
137
+ def communication_error(e)
138
+ return CommunicationError.new(e.message, e.response) if e.respond_to? :response
139
+ CommunicationError.new(e.message)
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,84 @@
1
+ require 'mailgun/exceptions/exceptions'
2
+
3
+ module Mailgun
4
+
5
+ # A Mailgun::Domains object is a simple CRUD interface to Mailgun Domains.
6
+ # Uses Mailgun
7
+ class Domains
8
+
9
+ # Public: creates a new Mailgun::Domains instance.
10
+ # Defaults to Mailgun::Client
11
+ def initialize(client = Mailgun::Client.new)
12
+ @client = client
13
+ end
14
+
15
+ # Public: Get Domains
16
+ #
17
+ # limit - [Integer] Maximum number of records to return. (100 by default)
18
+ # skip - [Integer] Number of records to skip. (0 by default)
19
+ #
20
+ # Returns [Array] A list of domains (hash)
21
+ def list(options = {})
22
+ @client.get('domains', options).to_h['items']
23
+ end
24
+ alias_method :get_domains, :list
25
+
26
+ # Public: Get domain information
27
+ #
28
+ # domain - [String] Domain name to lookup
29
+ #
30
+ # Returns [Hash] Information on the requested domains.
31
+ def info(domain)
32
+ fail(ParameterError, 'No domain given to find on Mailgun', caller) unless domain
33
+ @client.get("domains/#{domain}").to_h!
34
+ end
35
+ alias_method :get, :info
36
+ alias_method :get_domain, :info
37
+
38
+ # Public: Verify domain, update domain records
39
+ # Unknown status - this is not in the current Mailgun API
40
+ # Do no rely on this being available in future releases.
41
+ #
42
+ # domain - [String] Domain name
43
+ #
44
+ # Returns [Hash] Information on the updated/verified domains
45
+ def verify(domain)
46
+ fail(ParameterError, 'No domain given to verify on Mailgun', caller) unless domain
47
+ @client.put("domains/#{domain}/verify", nil).to_h!
48
+ end
49
+ alias_method :verify_domain, :verify
50
+
51
+ # Public: Add domain
52
+ #
53
+ # domain - [String] Name of the domain (ex. domain.com)
54
+ # options - [Hash] of
55
+ # smtp_password - [String] Password for SMTP authentication
56
+ # spam_action - [String] disabled or tag
57
+ # Disable, no spam filtering will occur for inbound messages.
58
+ # Tag, messages will be tagged wtih a spam header. See Spam Filter.
59
+ # wildcard - [Boolean] true or false Determines whether the domain will accept email for sub-domains.
60
+ #
61
+ # Returns [Hash] of created domain
62
+ def create(domain, options = {})
63
+ fail(ParameterError, 'No domain given to add on Mailgun', caller) unless domain
64
+ options = { smtp_password: nil, spam_action: 'disabled', wildcard: false }.merge(options)
65
+ options[:name] = domain
66
+ @client.post('domains', options).to_h
67
+ end
68
+ alias_method :add, :create
69
+ alias_method :add_domain, :create
70
+
71
+ # Public: Delete Domain
72
+ #
73
+ # domain - [String] domain name to delete (ex. domain.com)
74
+ #
75
+ # Returns [Boolean] if successful or not
76
+ def remove(domain)
77
+ fail(ParameterError, 'No domain given to remove on Mailgun', caller) unless domain
78
+ @client.delete("domains/#{domain}").to_h['message'] == 'Domain has been deleted'
79
+ end
80
+ alias_method :delete, :remove
81
+ alias_method :delete_domain, :remove
82
+
83
+ end
84
+ end
@@ -1,17 +1,21 @@
1
- require 'mailgun'
2
- require "mailgun/exceptions/exceptions"
3
-
1
+ require 'mailgun/exceptions/exceptions'
4
2
 
5
3
  module Mailgun
6
4
 
7
5
  # A Mailgun::Events object makes it really simple to consume
8
- # Mailgun's events from the Events endpoint.
6
+ # Mailgun's events from the Events endpoint.
9
7
  #
8
+ # This is not yet comprehensive.
10
9
  #
11
- # See the Github documentation for full examples.
12
-
10
+ # Examples
11
+ #
12
+ # See the Github documentation for full examples.
13
13
  class Events
14
14
 
15
+ # Public: event initializer
16
+ #
17
+ # client - an instance of Mailgun::Client
18
+ # domain - the domain to build queries
15
19
  def initialize(client, domain)
16
20
  @client = client
17
21
  @domain = domain
@@ -19,56 +23,70 @@ module Mailgun
19
23
  @paging_previous = nil
20
24
  end
21
25
 
22
- # Issues a simple get against the client.
26
+ # Public: Issues a simple get against the client.
23
27
  #
24
- # @param [Hash] params A hash of query options and/or filters.
25
- # @return [Mailgun::Response] Mailgun Response object.
26
-
27
- def get(params=nil)
28
- _get(params)
28
+ # params - a Hash of query options and/or filters.
29
+ #
30
+ # Returns a Mailgun::Response object.
31
+ def get(params = nil)
32
+ get_events(params)
29
33
  end
30
34
 
31
- # Using built in paging, obtains the next set of data.
35
+ # Public: Using built in paging, obtains the next set of data.
36
+ # If an events request hasn't been sent previously, this will send one
37
+ # without parameters
32
38
  #
33
- # @return [Mailgun::Response] Mailgun Response object.
34
-
35
- def next()
36
- _get(nil, @paging_next)
39
+ # Returns a Mailgun::Response object.
40
+ def next
41
+ get_events(nil, @paging_next)
37
42
  end
38
43
 
39
- # Using built in paging, obtains the previous set of data.
44
+ # Public: Using built in paging, obtains the previous set of data.
45
+ # If an events request hasn't been sent previously, this will send one
46
+ # without parameters
40
47
  #
41
- # @return [Mailgun::Response] Mailgun Response object.
42
-
43
- def previous()
44
- _get(nil, @paging_previous)
48
+ # Returns Mailgun::Response object.
49
+ def previous
50
+ get_events(nil, @paging_previous)
45
51
  end
46
52
 
47
53
  private
48
54
 
49
- def _get(params=nil, paging=nil)
55
+ # Internal: Makes and processes the event request through the client
56
+ #
57
+ # params - optional Hash of query options
58
+ # paging - the URL key used for previous/next requests
59
+ #
60
+ # Returns a Mailgun.Response object.
61
+ def get_events(params = nil, paging = nil)
50
62
  response = @client.get(construct_url(paging), params)
51
63
  extract_paging(response)
52
64
  response
53
65
  end
54
66
 
67
+ # Internal: given an event response, pull and store the paging keys
68
+ #
69
+ # response - a Mailgun::Response object
70
+ #
71
+ # Return is irrelevant.
55
72
  def extract_paging(response)
56
- paging_next = response.to_h["paging"]["next"]
57
- paging_previous = response.to_h["paging"]["previous"]
58
-
59
73
  # This is pretty hackish. But the URL will never change in API v2.
60
- @paging_next = paging_next.split("/")[6]
61
- @paging_previous = paging_previous.split("/")[6]
74
+ @paging_next = response.to_h['paging']['next'].split('/')[6]
75
+ @paging_previous = response.to_h['paging']['previous'].split('/')[6]
76
+ rescue
77
+ @paging_next = nil
78
+ @paging_previous = nil
62
79
  end
63
80
 
64
- def construct_url(paging=nil)
65
- if paging
66
- "#{@domain}/events/#{paging}"
67
- else
68
- "#{@domain}/events"
69
- end
81
+ # Internal: construct the event path to be used by the client
82
+ #
83
+ # paging - the URL key for previous/next set of results
84
+ #
85
+ # Returns a String of the partial URI
86
+ def construct_url(paging = nil)
87
+ return "#{@domain}/events/#{paging}" if paging
88
+ "#{@domain}/events"
70
89
  end
71
90
 
72
91
  end
73
-
74
92
  end
@@ -1,20 +1,53 @@
1
1
  module Mailgun
2
2
 
3
- class Error < RuntimeError
3
+ # Public: A basic class for mananging errors.
4
+ # Inherits from StandardError (previously RuntimeError) as not all errors are
5
+ # runtime errors.
6
+ class Error < StandardError
7
+
8
+ # Public: get an object an error is instantiated with
4
9
  attr_reader :object
5
10
 
6
- def initialize(message=nil, object=nil)
7
- @message = message
11
+ # Public: initialize a Mailgun:Error object
12
+ #
13
+ # message - a String describing the error
14
+ # object - an object with details about the error
15
+ def initialize(message = nil, object = nil)
16
+ super(message)
8
17
  @object = object
9
18
  end
10
-
11
- def to_s
12
- @message || self.class.to_s
13
- end
14
19
  end
15
20
 
16
- class ParameterError < Error; end
17
- class CommunicationError < Error; end
18
- class ParseError < Error; end
21
+ # Public: Class for managing parameter errors, with a pretty name.
22
+ # Inherits from Mailgun::Error
23
+ class ParameterError < Error; end
24
+
25
+ # Public: Class for managing parsing errors, with a pretty name.
26
+ # Inherits from Mailgun::Error
27
+ class ParseError < Error; end
28
+
29
+ # Public: Class for managing communications (eg http) response errors
30
+ # Inherits from Mailgun::Error
31
+ class CommunicationError < Error
32
+ # Public: gets HTTP status code
33
+ attr_reader :code
19
34
 
35
+ # Public: fallback if there is no response code on the object
36
+ NOCODE = 000
37
+
38
+ # Public: initialization of new error given a message and/or object
39
+ #
40
+ # message - a String detailing the error
41
+ # object - a RestClient::Reponse object
42
+ #
43
+ def initialize(message = nil, object = nil)
44
+ @object = object
45
+ @code = object.http_code || NOCODE
46
+ super(JSON.parse(object.body)['message'])
47
+ rescue NoMethodError, JSON::ParserError
48
+ @code = NOCODE
49
+ super(message)
50
+ end
51
+
52
+ end
20
53
  end
@@ -5,6 +5,10 @@ require 'openssl'
5
5
 
6
6
  module Mailgun
7
7
 
8
+ # Public: Provides methods for creating and handling opt-in URLs,
9
+ # particularlly for mailing lists.
10
+ #
11
+ # See: https://github.com/mailgun/mailgun-ruby/blob/master/OptInHandler.md
8
12
  class OptInHandler
9
13
 
10
14
  # Generates a hash that can be used to validate opt-in recipients. Encodes
@@ -14,22 +18,19 @@ module Mailgun
14
18
  # @param [String] secret_app_id A secret passphrase used as a constant for the hash.
15
19
  # @param [Hash] recipient_address The address of the user that should be subscribed.
16
20
  # @return [String] A url encoded URL suffix hash.
17
-
18
21
  def self.generate_hash(mailing_list, secret_app_id, recipient_address)
19
- innerPayload = {'l' => mailing_list,
20
- 'r' => recipient_address}
22
+ inner_payload = { 'l' => mailing_list, 'r' => recipient_address }
21
23
 
22
- innerPayloadEncoded = Base64.encode64(JSON.generate(innerPayload))
24
+ inner_payload_encoded = Base64.encode64(JSON.generate(inner_payload))
23
25
 
24
26
  sha1_digest = OpenSSL::Digest.new('sha1')
25
- digest = OpenSSL::HMAC.hexdigest(sha1_digest, secret_app_id, innerPayloadEncoded)
27
+ digest = OpenSSL::HMAC.hexdigest(sha1_digest, secret_app_id, inner_payload_encoded)
26
28
 
27
- outerPayload = {'h' => digest,
28
- 'p' => innerPayloadEncoded}
29
+ outer_payload = { 'h' => digest, 'p' => inner_payload_encoded }
29
30
 
30
- outerPayloadEncoded = Base64.encode64(JSON.generate(outerPayload))
31
+ outer_payload_encoded = Base64.encode64(JSON.generate(outer_payload))
31
32
 
32
- URI.escape(outerPayloadEncoded)
33
+ CGI.escape(outer_payload_encoded)
33
34
  end
34
35
 
35
36
  # Validates the hash provided from the generate_hash method.
@@ -37,24 +38,22 @@ module Mailgun
37
38
  # @param [String] secret_app_id A secret passphrase used as a constant for the hash.
38
39
  # @param [Hash] unique_hash The hash from the user. Likely via link click.
39
40
  # @return [Hash or Boolean] A hash with 'recipient_address' and 'mailing_list', if validates. Otherwise, boolean false.
40
-
41
41
  def self.validate_hash(secret_app_id, unique_hash)
42
- outerPayload = JSON.parse(Base64.decode64(URI.unescape(unique_hash)))
42
+ outer_payload = JSON.parse(Base64.decode64(CGI.unescape(unique_hash)))
43
43
 
44
44
  sha1_digest = OpenSSL::Digest.new('sha1')
45
- generated_hash = OpenSSL::HMAC.hexdigest(sha1_digest, secret_app_id, outerPayload['p'])
45
+ generated_hash = OpenSSL::HMAC.hexdigest(sha1_digest, secret_app_id, outer_payload['p'])
46
46
 
47
- innerPayload = JSON.parse(Base64.decode64(URI.unescape(outerPayload['p'])))
47
+ inner_payload = JSON.parse(Base64.decode64(CGI.unescape(outer_payload['p'])))
48
48
 
49
- hash_provided = outerPayload['h']
49
+ hash_provided = outer_payload['h']
50
50
 
51
- if(generated_hash == hash_provided)
52
- return {'recipient_address' => innerPayload['r'], 'mailing_list' => innerPayload['l']}
53
- else
54
- return false
51
+ if generated_hash == hash_provided
52
+ return { 'recipient_address' => inner_payload['r'], 'mailing_list' => inner_payload['l'] }
55
53
  end
54
+ false
56
55
  end
57
56
 
58
57
  end
59
-
58
+
60
59
  end