mailgun-ruby 1.0.3 → 1.1.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.
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