wj-mailgun-ruby 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rubocop.yml +8 -0
  4. data/.rubocop_todo.yml +22 -0
  5. data/.ruby-env.yml.example +12 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +24 -0
  8. data/Gemfile +6 -0
  9. data/LICENSE +191 -0
  10. data/README.md +241 -0
  11. data/Rakefile +35 -0
  12. data/docs/Domains.md +54 -0
  13. data/docs/Events.md +46 -0
  14. data/docs/MessageBuilder.md +105 -0
  15. data/docs/Messages.md +107 -0
  16. data/docs/OptInHandler.md +103 -0
  17. data/docs/Snippets.md +526 -0
  18. data/docs/Suppressions.md +82 -0
  19. data/docs/Webhooks.md +40 -0
  20. data/lib/mailgun-ruby.rb +2 -0
  21. data/lib/mailgun.rb +39 -0
  22. data/lib/mailgun/address.rb +45 -0
  23. data/lib/mailgun/chains.rb +16 -0
  24. data/lib/mailgun/client.rb +199 -0
  25. data/lib/mailgun/domains/domains.rb +84 -0
  26. data/lib/mailgun/events/events.rb +120 -0
  27. data/lib/mailgun/exceptions/exceptions.rb +65 -0
  28. data/lib/mailgun/lists/opt_in_handler.rb +58 -0
  29. data/lib/mailgun/messages/batch_message.rb +125 -0
  30. data/lib/mailgun/messages/message_builder.rb +413 -0
  31. data/lib/mailgun/response.rb +62 -0
  32. data/lib/mailgun/suppressions.rb +270 -0
  33. data/lib/mailgun/version.rb +4 -0
  34. data/lib/mailgun/webhooks/webhooks.rb +101 -0
  35. data/lib/railgun.rb +8 -0
  36. data/lib/railgun/attachment.rb +56 -0
  37. data/lib/railgun/errors.rb +27 -0
  38. data/lib/railgun/mailer.rb +161 -0
  39. data/lib/railgun/message.rb +17 -0
  40. data/lib/railgun/railtie.rb +9 -0
  41. data/mailgun.gemspec +37 -0
  42. data/spec/integration/bounces_spec.rb +44 -0
  43. data/spec/integration/campaign_spec.rb +60 -0
  44. data/spec/integration/complaints_spec.rb +38 -0
  45. data/spec/integration/domains_spec.rb +39 -0
  46. data/spec/integration/email_validation_spec.rb +57 -0
  47. data/spec/integration/events_spec.rb +28 -0
  48. data/spec/integration/list_members_spec.rb +63 -0
  49. data/spec/integration/list_spec.rb +58 -0
  50. data/spec/integration/mailgun_spec.rb +121 -0
  51. data/spec/integration/messages/sample_data/mime.txt +38 -0
  52. data/spec/integration/routes_spec.rb +74 -0
  53. data/spec/integration/stats_spec.rb +15 -0
  54. data/spec/integration/suppressions_spec.rb +126 -0
  55. data/spec/integration/unsubscribes_spec.rb +42 -0
  56. data/spec/integration/webhook_spec.rb +54 -0
  57. data/spec/spec_helper.rb +45 -0
  58. data/spec/unit/connection/test_client.rb +99 -0
  59. data/spec/unit/events/events_spec.rb +50 -0
  60. data/spec/unit/lists/opt_in_handler_spec.rb +24 -0
  61. data/spec/unit/mailgun_spec.rb +127 -0
  62. data/spec/unit/messages/batch_message_spec.rb +131 -0
  63. data/spec/unit/messages/message_builder_spec.rb +584 -0
  64. data/spec/unit/messages/sample_data/mailgun_icon.png +0 -0
  65. data/spec/unit/messages/sample_data/mime.txt +38 -0
  66. data/spec/unit/messages/sample_data/rackspace_logo.jpg +0 -0
  67. data/vcr_cassettes/bounces.yml +175 -0
  68. data/vcr_cassettes/complaints.yml +175 -0
  69. data/vcr_cassettes/domains.todo.yml +42 -0
  70. data/vcr_cassettes/domains.yml +360 -0
  71. data/vcr_cassettes/email_validation.yml +167 -0
  72. data/vcr_cassettes/events.yml +108 -0
  73. data/vcr_cassettes/exceptions.yml +45 -0
  74. data/vcr_cassettes/list_members.yml +320 -0
  75. data/vcr_cassettes/mailing_list.todo.yml +43 -0
  76. data/vcr_cassettes/mailing_list.yml +390 -0
  77. data/vcr_cassettes/routes.yml +359 -0
  78. data/vcr_cassettes/send_message.yml +107 -0
  79. data/vcr_cassettes/stats.yml +44 -0
  80. data/vcr_cassettes/suppressions.yml +676 -0
  81. data/vcr_cassettes/unsubscribes.yml +191 -0
  82. data/vcr_cassettes/webhooks.yml +276 -0
  83. metadata +263 -0
@@ -0,0 +1,120 @@
1
+ require 'mailgun/exceptions/exceptions'
2
+
3
+ module Mailgun
4
+
5
+ # A Mailgun::Events object makes it really simple to consume
6
+ # Mailgun's events from the Events endpoint.
7
+ #
8
+ # This is not yet comprehensive.
9
+ #
10
+ # Examples
11
+ #
12
+ # See the Github documentation for full examples.
13
+ class Events
14
+ include Enumerable
15
+
16
+ # Public: event initializer
17
+ #
18
+ # client - an instance of Mailgun::Client
19
+ # domain - the domain to build queries
20
+ def initialize(client, domain)
21
+ @client = client
22
+ @domain = domain
23
+ @paging_next = nil
24
+ @paging_previous = nil
25
+ end
26
+
27
+ # Public: Issues a simple get against the client. Alias of `next`.
28
+ #
29
+ # params - a Hash of query options and/or filters.
30
+ #
31
+ # Returns a Mailgun::Response object.
32
+ def get(params = nil)
33
+ self.next(params)
34
+ end
35
+
36
+ # Public: Using built in paging, obtains the next set of data.
37
+ # If an events request hasn't been sent previously, this will send one
38
+ # without parameters
39
+ #
40
+ # params - a Hash of query options and/or filters.
41
+ #
42
+ # Returns a Mailgun::Response object.
43
+ def next(params = nil)
44
+ get_events(params, @paging_next)
45
+ end
46
+
47
+ # Public: Using built in paging, obtains the previous set of data.
48
+ # If an events request hasn't been sent previously, this will send one
49
+ # without parameters
50
+ #
51
+ # params - a Hash of query options and/or filters.
52
+ #
53
+ # Returns Mailgun::Response object.
54
+ def previous(params = nil)
55
+ get_events(params, @paging_previous)
56
+ end
57
+
58
+ # Public: Allows iterating through all events and performs automatic paging.
59
+ #
60
+ # &block - Block to execute on items.
61
+ def each(&block)
62
+ items = self.next.to_h['items']
63
+
64
+ until items.empty?
65
+ items.each(&block)
66
+ items = self.next.to_h['items']
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Internal: Makes and processes the event request through the client
73
+ #
74
+ # params - optional Hash of query options
75
+ # paging - the URL key used for previous/next requests
76
+ #
77
+ # Returns a Mailgun.Response object.
78
+ def get_events(params = nil, paging = nil)
79
+ response = @client.get(construct_url(paging), params)
80
+ extract_paging(response)
81
+ response
82
+ end
83
+
84
+ # Internal: given an event response, pull and store the paging keys
85
+ #
86
+ # response - a Mailgun::Response object
87
+ #
88
+ # Return is irrelevant.
89
+ def extract_paging(response)
90
+ paging = response.to_h['paging']
91
+ next_page_url = paging && paging['next'] # gives nil when any one of the keys doens't exist
92
+ previous_page_url = paging && paging['previous'] # can be replaced with Hash#dig for ruby >= 2.3.0
93
+ @paging_next = extract_endpoint_from(next_page_url)
94
+ @paging_previous = extract_endpoint_from(previous_page_url)
95
+ end
96
+
97
+ # Internal: given a paging URL, extract the endpoint
98
+ #
99
+ # response - the endpoint for the previous/next page
100
+ #
101
+ # Returns a String of the partial URI if the given url follows the regular API format
102
+ # Returns nil in other cases (e.g. when given nil, or an irrelevant url)
103
+ def extract_endpoint_from(url = nil)
104
+ URI.parse(url).path[/api.mailgun.net\/v[\d]\/#{@domain}\/events\/(.+)/,1]
105
+ rescue URI::InvalidURIError
106
+ nil
107
+ end
108
+
109
+ # Internal: construct the event path to be used by the client
110
+ #
111
+ # paging - the URL key for previous/next set of results
112
+ #
113
+ # Returns a String of the partial URI
114
+ def construct_url(paging = nil)
115
+ return "#{@domain}/events/#{paging}" if paging
116
+ "#{@domain}/events"
117
+ end
118
+
119
+ end
120
+ end
@@ -0,0 +1,65 @@
1
+ module Mailgun
2
+
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
9
+ attr_reader :object
10
+
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)
17
+ @object = object
18
+ end
19
+ end
20
+
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
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
+ # response - a RestClient::Response object
42
+ #
43
+ def initialize(message = nil, response = nil)
44
+ @response = response
45
+ @code = response.code || NOCODE
46
+
47
+ begin
48
+ api_message = JSON.parse(response.body)['message']
49
+ rescue JSON::ParserError
50
+ api_message = response.body
51
+ rescue NoMethodError
52
+ api_message = "Unknown API error"
53
+ end
54
+
55
+ message = message || ''
56
+ message = message + ': ' + api_message
57
+
58
+ super(message, response)
59
+ rescue NoMethodError, JSON::ParserError
60
+ @code = NOCODE
61
+ super(message, response)
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,58 @@
1
+ require 'uri'
2
+ require 'base64'
3
+ require 'openssl'
4
+
5
+ module Mailgun
6
+
7
+ # Public: Provides methods for creating and handling opt-in URLs,
8
+ # particularlly for mailing lists.
9
+ #
10
+ # See: https://github.com/mailgun/mailgun-ruby/blob/master/OptInHandler.md
11
+ class OptInHandler
12
+
13
+ # Generates a hash that can be used to validate opt-in recipients. Encodes
14
+ # all the necessary data in the URL.
15
+ #
16
+ # @param [String] mailing_list The mailing list the user should be subscribed to.
17
+ # @param [String] secret_app_id A secret passphrase used as a constant for the hash.
18
+ # @param [Hash] recipient_address The address of the user that should be subscribed.
19
+ # @return [String] A url encoded URL suffix hash.
20
+ def self.generate_hash(mailing_list, secret_app_id, recipient_address)
21
+ inner_payload = { 'l' => mailing_list, 'r' => recipient_address }
22
+
23
+ inner_payload_encoded = Base64.encode64(JSON.generate(inner_payload))
24
+
25
+ sha1_digest = OpenSSL::Digest.new('sha1')
26
+ digest = OpenSSL::HMAC.hexdigest(sha1_digest, secret_app_id, inner_payload_encoded)
27
+
28
+ outer_payload = { 'h' => digest, 'p' => inner_payload_encoded }
29
+
30
+ outer_payload_encoded = Base64.encode64(JSON.generate(outer_payload))
31
+
32
+ CGI.escape(outer_payload_encoded)
33
+ end
34
+
35
+ # Validates the hash provided from the generate_hash method.
36
+ #
37
+ # @param [String] secret_app_id A secret passphrase used as a constant for the hash.
38
+ # @param [Hash] unique_hash The hash from the user. Likely via link click.
39
+ # @return [Hash or Boolean] A hash with 'recipient_address' and 'mailing_list', if validates. Otherwise, boolean false.
40
+ def self.validate_hash(secret_app_id, unique_hash)
41
+ outer_payload = JSON.parse(Base64.decode64(CGI.unescape(unique_hash)))
42
+
43
+ sha1_digest = OpenSSL::Digest.new('sha1')
44
+ generated_hash = OpenSSL::HMAC.hexdigest(sha1_digest, secret_app_id, outer_payload['p'])
45
+
46
+ inner_payload = JSON.parse(Base64.decode64(CGI.unescape(outer_payload['p'])))
47
+
48
+ hash_provided = outer_payload['h']
49
+
50
+ if generated_hash == hash_provided
51
+ return { 'recipient_address' => inner_payload['r'], 'mailing_list' => inner_payload['l'] }
52
+ end
53
+ false
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,125 @@
1
+ require 'mailgun/messages/message_builder'
2
+
3
+ module Mailgun
4
+
5
+ # A Mailgun::BatchMessage object is used to create a valid payload
6
+ # for Batch Sending. Batch Sending can be difficult to implement, therefore
7
+ # this code makes it dead simple to send millions of messages in batches of
8
+ # 1,000 recipients per HTTP call.
9
+ #
10
+ # For the curious, the class simply keeps track of recipient data (count,
11
+ # user variables), and fires the API payload on the 1,000th addition of a recipient.
12
+ #
13
+ # The best way to use this class is:
14
+ # 1. Build your message using the Message Builder methods.
15
+ # 2. Query your source and create an iterable list.
16
+ # 3. Iterate through your source data, and add your recipients using the
17
+ # add_recipient() method.
18
+ # 4. Call finalize() to flush any remaining recipients and obtain/store
19
+ # the message_ids for tracking purposes.
20
+ #
21
+ # See the Github documentation for full examples.
22
+ class BatchMessage < MessageBuilder
23
+
24
+ attr_reader :message_ids, :domain, :recipient_variables
25
+
26
+ # Public: Creates a new BatchMessage object.
27
+ def initialize(client, domain)
28
+ @client = client
29
+ @recipient_variables = {}
30
+ @domain = domain
31
+ @message_ids = {}
32
+ @message = Hash.new { |hash, key| hash[key] = [] }
33
+
34
+ @counters = {
35
+ recipients: { to: 0, cc: 0, bcc: 0 },
36
+ attributes: { attachment: 0, campaign_id: 0, custom_option: 0, tag: 0 }
37
+ }
38
+ end
39
+
40
+ # Adds a specific type of recipient to the batch message object.
41
+ #
42
+ # @param [String] recipient_type The type of recipient. "to".
43
+ # @param [String] address The email address of the recipient to add to the message object.
44
+ # @param [Hash] variables A hash of the variables associated with the recipient. We recommend "first" and "last" at a minimum!
45
+ # @return [void]
46
+ def add_recipient(recipient_type, address, variables = nil)
47
+ # send the message when we have 1000, not before
48
+ send_message if @counters[:recipients][recipient_type] == Mailgun::Chains::MAX_RECIPIENTS
49
+
50
+ compiled_address = parse_address(address, variables)
51
+ set_multi_complex(recipient_type, compiled_address)
52
+
53
+ store_recipient_variables(recipient_type, address, variables) if recipient_type != :from
54
+
55
+ @counters[:recipients][recipient_type] += 1 if @counters[:recipients].key?(recipient_type)
56
+ end
57
+
58
+ # Always call this function after adding recipients. If less than 1000 are added,
59
+ # this function will ensure the batch is sent.
60
+ #
61
+ # @return [Hash] A hash of {'Message ID' => '# of Messages Sent'}
62
+ def finalize
63
+ send_message if any_recipients_left?
64
+ @message_ids
65
+ end
66
+
67
+ private
68
+
69
+ # This method determines if it's necessary to send another batch.
70
+ #
71
+ # @return [Boolean]
72
+ def any_recipients_left?
73
+ return true if @counters[:recipients][:to] > 0
74
+ return true if @counters[:recipients][:cc] > 0
75
+ return true if @counters[:recipients][:bcc] > 0
76
+ false
77
+ end
78
+
79
+ # This method initiates a batch send to the API. It formats the recipient
80
+ # variables, posts to the API, gathers the message IDs, then flushes that data
81
+ # to prepare for the next batch. This method implements the Mailgun Client, thus,
82
+ # an exception will be thrown if a communication error occurs.
83
+ #
84
+ # @return [Boolean]
85
+ def send_message
86
+ rkey = 'recipient-variables'
87
+ set_multi_simple rkey, JSON.generate(@recipient_variables)
88
+ @message[rkey] = @message[rkey].first if @message.key?(rkey)
89
+
90
+ response = @client.send_message(@domain, @message).to_h!
91
+ message_id = response['id'].gsub(/\>|\</, '')
92
+ @message_ids[message_id] = count_recipients
93
+ reset_message
94
+ end
95
+
96
+ # This method stores recipient variables for each recipient added, if
97
+ # variables exist.
98
+ def store_recipient_variables(recipient_type, address, variables)
99
+ variables = { id: @counters[:recipients][recipient_type] } unless variables
100
+ @recipient_variables[address] = variables
101
+ end
102
+
103
+ # This method stores recipient variables for each recipient added, if
104
+ # variables exist.
105
+ def count_recipients
106
+ count = 0
107
+ @counters[:recipients].each_value { |cnt| count += cnt }
108
+ count
109
+ end
110
+
111
+ # This method resets the message object to prepare for the next batch
112
+ # of recipients.
113
+ def reset_message
114
+ @message.delete('recipient-variables')
115
+ @message.delete(:to)
116
+ @message.delete(:cc)
117
+ @message.delete(:bcc)
118
+ @counters[:recipients][:to] = 0
119
+ @counters[:recipients][:cc] = 0
120
+ @counters[:recipients][:bcc] = 0
121
+ end
122
+
123
+ end
124
+
125
+ end
@@ -0,0 +1,413 @@
1
+ require 'time'
2
+
3
+ module Mailgun
4
+
5
+ # A Mailgun::MessageBuilder object is used to create a valid payload
6
+ # for the Mailgun API messages endpoint. If you prefer step by step message
7
+ # generation through your code, this class is for you.
8
+ #
9
+ # See the Github documentation for full examples.
10
+ class MessageBuilder
11
+
12
+ attr_reader :message, :counters
13
+
14
+ # Public: Creates a new MessageBuilder object.
15
+ def initialize
16
+ @message = Hash.new { |hash, key| hash[key] = [] }
17
+
18
+ @counters = {
19
+ recipients: { to: 0, cc: 0, bcc: 0 },
20
+ attributes: { attachment: 0, campaign_id: 0, custom_option: 0, tag: 0 }
21
+ }
22
+ end
23
+
24
+ # Adds a specific type of recipient to the message object.
25
+ #
26
+ # WARNING: Setting 'h:reply-to' with add_recipient() is deprecated! Use 'reply_to' instead.
27
+ #
28
+ # @param [String] recipient_type The type of recipient. "to", "cc", "bcc" or "h:reply-to".
29
+ # @param [String] address The email address of the recipient to add to the message object.
30
+ # @param [Hash] variables A hash of the variables associated with the recipient. We recommend "first" and "last" at a minimum!
31
+ # @return [void]
32
+ def add_recipient(recipient_type, address, variables = nil)
33
+ if recipient_type == "h:reply-to"
34
+ warn 'DEPRECATION: "add_recipient("h:reply-to", ...)" is deprecated. Please use "reply_to" instead.'
35
+ return reply_to(address, variables)
36
+ end
37
+
38
+ if (@counters[:recipients][recipient_type] || 0) >= Mailgun::Chains::MAX_RECIPIENTS
39
+ fail Mailgun::ParameterError, 'Too many recipients added to message.', address
40
+ end
41
+
42
+ compiled_address = parse_address(address, variables)
43
+ set_multi_complex(recipient_type, compiled_address)
44
+
45
+ @counters[:recipients][recipient_type] += 1 if @counters[:recipients].key?(recipient_type)
46
+ end
47
+
48
+ # Sets the from address for the message
49
+ #
50
+ # @param [String] address The address of the sender.
51
+ # @param [Hash] variables A hash of the variables associated with the recipient. We recommend "first" and "last" at a minimum!
52
+ # @return [void]
53
+ def from(address, vars = nil)
54
+ add_recipient(:from, address, vars)
55
+ end
56
+
57
+ # Deprecated: please use 'from' instead.
58
+ def set_from_address(address, variables = nil)
59
+ warn 'DEPRECATION: "set_from_address" is deprecated. Please use "from" instead.'
60
+ from(address, variables)
61
+ end
62
+
63
+ # Set the message's Reply-To address.
64
+ #
65
+ # Rationale: According to RFC, only one Reply-To address is allowed, so it
66
+ # is *okay* to bypass the set_multi_simple and set reply-to directly.
67
+ #
68
+ # @param [String] address The email address to provide as Reply-To.
69
+ # @param [Hash] variables A hash of variables associated with the recipient.
70
+ # @return [void]
71
+ def reply_to(address, variables = nil)
72
+ compiled_address = parse_address(address, variables)
73
+ header("reply-to", compiled_address)
74
+ end
75
+
76
+ # Set a subject for the message object
77
+ #
78
+ # @param [String] subject The subject for the email.
79
+ # @return [void]
80
+ def subject(subj = nil)
81
+ set_multi_simple(:subject, subj)
82
+ end
83
+
84
+ # Deprecated: Please use "subject" instead.
85
+ def set_subject(subj = nil)
86
+ warn 'DEPRECATION: "set_subject" is deprecated. Please use "subject" instead.'
87
+ subject(subj)
88
+ end
89
+
90
+ # Set a text body for the message object
91
+ #
92
+ # @param [String] text_body The text body for the email.
93
+ # @return [void]
94
+ def body_text(text_body = nil)
95
+ set_multi_simple(:text, text_body)
96
+ end
97
+
98
+ # Deprecated: Please use "body_text" instead.
99
+ def set_text_body(text_body = nil)
100
+ warn 'DEPRECATION: "set_text_body" is deprecated. Please use "body_text" instead.'
101
+ body_text(text_body)
102
+ end
103
+
104
+ # Set a html body for the message object
105
+ #
106
+ # @param [String] html_body The html body for the email.
107
+ # @return [void]
108
+ def body_html(html_body = nil)
109
+ set_multi_simple(:html, html_body)
110
+ end
111
+
112
+ # Deprecated: Please use "body_html" instead.
113
+ def set_html_body(html_body = nil)
114
+ warn 'DEPRECATION: "set_html_body" is deprecated. Please use "body_html" instead.'
115
+ body_html(html_body)
116
+ end
117
+
118
+ # Adds a series of attachments, when called upon.
119
+ #
120
+ # @param [String|File] attachment A file object for attaching as an attachment.
121
+ # @param [String] filename The filename you wish the attachment to be.
122
+ # @return [void]
123
+ def add_attachment(attachment, filename = nil)
124
+ add_file(:attachment, attachment, filename)
125
+ end
126
+
127
+ # Adds an inline image to the mesage object.
128
+ #
129
+ # @param [String|File] inline_image A file object for attaching an inline image.
130
+ # @param [String] filename The filename you wish the inline image to be.
131
+ # @return [void]
132
+ def add_inline_image(inline_image, filename = nil)
133
+ add_file(:inline, inline_image, filename)
134
+ end
135
+
136
+ # Adds a List-Unsubscribe for the message header.
137
+ #
138
+ # @param [Array<String>] *variables Any number of url or mailto
139
+ # @return [void]
140
+ def list_unsubscribe(*variables)
141
+ set_single('h:List-Unsubscribe', variables.map { |var| "<#{var}>" }.join(','))
142
+ end
143
+
144
+ # Send a message in test mode. (The message won't really be sent to the recipient)
145
+ #
146
+ # @param [Boolean] mode The boolean or string value (will fix itself)
147
+ # @return [void]
148
+ def test_mode(mode)
149
+ set_multi_simple('o:testmode', bool_lookup(mode))
150
+ end
151
+
152
+ # Deprecated: 'set_test_mode' is depreciated. Please use 'test_mode' instead.
153
+ def set_test_mode(mode)
154
+ warn 'DEPRECATION: "set_test_mode" is deprecated. Please use "test_mode" instead.'
155
+ test_mode(mode)
156
+ end
157
+
158
+ # Turn DKIM on or off per message
159
+ #
160
+ # @param [Boolean] mode The boolean or string value(will fix itself)
161
+ # @return [void]
162
+ def dkim(mode)
163
+ set_multi_simple('o:dkim', bool_lookup(mode))
164
+ end
165
+
166
+ # Deprecated: 'set_dkim' is deprecated. Please use 'dkim' instead.
167
+ def set_dkim(mode)
168
+ warn 'DEPRECATION: "set_dkim" is deprecated. Please use "dkim" instead.'
169
+ dkim(mode)
170
+ end
171
+
172
+ # Add campaign IDs to message. Limit of 3 per message.
173
+ #
174
+ # @param [String] campaign_id A defined campaign ID to add to the message.
175
+ # @return [void]
176
+ def add_campaign_id(campaign_id)
177
+ fail(Mailgun::ParameterError, 'Too many campaigns added to message.', campaign_id) if @counters[:attributes][:campaign_id] >= Mailgun::Chains::MAX_CAMPAIGN_IDS
178
+
179
+ set_multi_complex('o:campaign', campaign_id)
180
+ @counters[:attributes][:campaign_id] += 1
181
+ end
182
+
183
+ # Add tags to message. Limit of 3 per message.
184
+ #
185
+ # @param [String] tag A defined campaign ID to add to the message.
186
+ # @return [void]
187
+ def add_tag(tag)
188
+ if @counters[:attributes][:tag] >= Mailgun::Chains::MAX_TAGS
189
+ fail Mailgun::ParameterError, 'Too many tags added to message.', tag
190
+ end
191
+ set_multi_complex('o:tag', tag)
192
+ @counters[:attributes][:tag] += 1
193
+ end
194
+
195
+ # Turn Open Tracking on and off, on a per message basis.
196
+ #
197
+ # @param [Boolean] tracking Boolean true or false.
198
+ # @return [void]
199
+ def track_opens(mode)
200
+ set_multi_simple('o:tracking-opens', bool_lookup(mode))
201
+ end
202
+
203
+ # Deprecated: 'set_open_tracking' is deprecated. Please use 'track_opens' instead.
204
+ def set_open_tracking(tracking)
205
+ warn 'DEPRECATION: "set_open_tracking" is deprecated. Please use "track_opens" instead.'
206
+ track_opens(tracking)
207
+ end
208
+
209
+ # Turn Click Tracking on and off, on a per message basis.
210
+ #
211
+ # @param [String] mode True, False, or HTML (for HTML only tracking)
212
+ # @return [void]
213
+ def track_clicks(mode)
214
+ set_multi_simple('o:tracking-clicks', bool_lookup(mode))
215
+ end
216
+
217
+ # Depreciated: 'set_click_tracking. is deprecated. Please use 'track_clicks' instead.
218
+ def set_click_tracking(tracking)
219
+ warn 'DEPRECATION: "set_click_tracking" is deprecated. Please use "track_clicks" instead.'
220
+ track_clicks(tracking)
221
+ end
222
+
223
+ # Enable Delivery delay on message. Specify an RFC2822 date, and Mailgun
224
+ # will not deliver the message until that date/time. For conversion
225
+ # options, see Ruby "Time". Example: "October 25, 2013 10:00PM CST" will
226
+ # be converted to "Fri, 25 Oct 2013 22:00:00 -0600".
227
+ #
228
+ # @param [String] timestamp A date and time, including a timezone.
229
+ # @return [void]
230
+ def deliver_at(timestamp)
231
+ time_str = DateTime.parse(timestamp)
232
+ set_multi_simple('o:deliverytime', time_str.rfc2822)
233
+ end
234
+
235
+ # Deprecated: 'set_delivery_time' is deprecated. Please use 'deliver_at' instead.
236
+ def set_delivery_time(timestamp)
237
+ warn 'DEPRECATION: "set_delivery_time" is deprecated. Please use "deliver_at" instead.'
238
+ deliver_at timestamp
239
+ end
240
+
241
+ # Add custom data to the message. The data should be either a hash or JSON
242
+ # encoded. The custom data will be added as a header to your message.
243
+ #
244
+ # @param [string] name A name for the custom data. (Ex. X-Mailgun-<Name of Data>: {})
245
+ # @param [Hash] data Either a hash or JSON string.
246
+ # @return [void]
247
+ def header(name, data)
248
+ fail(Mailgun::ParameterError, 'Header name for message must be specified') if name.to_s.empty?
249
+ begin
250
+ jsondata = make_json data
251
+ set_single("h:#{name}", jsondata)
252
+ rescue Mailgun::ParameterError
253
+ set_single("h:#{name}", data)
254
+ end
255
+ end
256
+
257
+ # Deprecated: 'set_custom_data' is deprecated. Please use 'header' instead.
258
+ def set_custom_data(name, data)
259
+ warn 'DEPRECATION: "set_custom_data" is deprecated. Please use "header" instead.'
260
+ header name, data
261
+ end
262
+
263
+ # Attaches custom JSON data to the message. See the following doc page for more info.
264
+ # https://documentation.mailgun.com/user_manual.html#attaching-data-to-messages
265
+ #
266
+ # @param [String] name A name for the custom variable block.
267
+ # @param [String|Hash] data Either a string or a hash. If it is not valid JSON or
268
+ # can not be converted to JSON, ParameterError will be raised.
269
+ # @return [void]
270
+ def variable(name, data)
271
+ fail(Mailgun::ParameterError, 'Variable name must be specified') if name.to_s.empty?
272
+ jsondata = make_json data
273
+ set_single("v:#{name}", jsondata)
274
+ end
275
+
276
+ # Add custom parameter to the message. A custom parameter is any parameter that
277
+ # is not yet supported by the SDK, but available at the API. Note: No validation
278
+ # is performed. Don't forget to prefix the parameter with o, h, or v.
279
+ #
280
+ # @param [string] name A name for the custom parameter.
281
+ # @param [string] data A string of data for the parameter.
282
+ # @return [void]
283
+ def add_custom_parameter(name, data)
284
+ set_multi_complex(name, data)
285
+ end
286
+
287
+ # Set the Message-Id header to a custom value. Don't forget to enclose the
288
+ # Message-Id in angle brackets, and ensure the @domain is present. Doesn't
289
+ # use simple or complex setters because it should not set value in an array.
290
+ #
291
+ # @param [string] data A string of data for the parameter. Passing nil or
292
+ # empty string will delete h:Message-Id key and value from @message hash.
293
+ # @return [void]
294
+ def message_id(data = nil)
295
+ key = 'h:Message-Id'
296
+ return @message.delete(key) if data.to_s.empty?
297
+ set_single(key, data)
298
+ end
299
+
300
+ # Deprecated: 'set_message_id' is deprecated. Use 'message_id' instead.
301
+ def set_message_id(data = nil)
302
+ warn 'DEPRECATION: "set_message_id" is deprecated. Please use "message_id" instead.'
303
+ message_id data
304
+ end
305
+
306
+ private
307
+
308
+ # Sets a single value in the message hash where "multidict" features are not needed.
309
+ # Does *not* permit duplicate params.
310
+ #
311
+ # @param [String] parameter The message object parameter name.
312
+ # @param [String] value The value of the parameter.
313
+ # @return [void]
314
+ def set_single(parameter, value)
315
+ @message[parameter] = value ? value : ''
316
+ end
317
+
318
+ # Sets values within the multidict, however, prevents
319
+ # duplicate values for keys.
320
+ #
321
+ # @param [String] parameter The message object parameter name.
322
+ # @param [String] value The value of the parameter.
323
+ # @return [void]
324
+ def set_multi_simple(parameter, value)
325
+ @message[parameter] = value ? [value] : ['']
326
+ end
327
+
328
+ # Sets values within the multidict, however, allows
329
+ # duplicate values for keys.
330
+ #
331
+ # @param [String] parameter The message object parameter name.
332
+ # @param [String] value The value of the parameter.
333
+ # @return [void]
334
+ def set_multi_complex(parameter, value)
335
+ @message[parameter] << (value || '')
336
+ end
337
+
338
+ # Converts boolean type to string
339
+ #
340
+ # @param [String] value The item to convert
341
+ # @return [void]
342
+ def bool_lookup(value)
343
+ return 'yes' if %w(true yes yep).include? value.to_s.downcase
344
+ return 'no' if %w(false no nope).include? value.to_s.downcase
345
+ value
346
+ end
347
+
348
+ # Validates whether the input is JSON.
349
+ #
350
+ # @param [String] json_ The suspected JSON string.
351
+ # @return [void]
352
+ def valid_json?(json_)
353
+ JSON.parse(json_)
354
+ return true
355
+ rescue JSON::ParserError
356
+ false
357
+ end
358
+
359
+ # Private: given an object attempt to make it into JSON
360
+ #
361
+ # obj - an object. Hopefully a JSON string or Hash
362
+ #
363
+ # Returns a JSON object or raises ParameterError
364
+ def make_json(obj)
365
+ return JSON.parse(obj).to_json if obj.is_a?(String)
366
+ return obj.to_json if obj.is_a?(Hash)
367
+ return JSON.generate(obj).to_json
368
+ rescue
369
+ raise Mailgun::ParameterError, 'Provided data could not be made into JSON. Try a JSON string or Hash.', obj
370
+ end
371
+
372
+ # Parses the address and gracefully handles any
373
+ # missing parameters. The result should be something like:
374
+ # "'First Last' <person@domain.com>"
375
+ #
376
+ # @param [String] address The email address to parse.
377
+ # @param [Hash] variables A list of recipient variables.
378
+ # @return [void]
379
+ def parse_address(address, vars)
380
+ return address unless vars.is_a? Hash
381
+ fail(Mailgun::ParameterError, 'Email address not specified') unless address.is_a? String
382
+
383
+ full_name = "#{vars['first']} #{vars['last']}".strip
384
+
385
+ return "'#{full_name}' <#{address}>" if defined?(full_name)
386
+ address
387
+ end
388
+
389
+ # Private: Adds a file to the message.
390
+ #
391
+ # @param [Symbol] disposition The type of file: :attachment or :inline
392
+ # @param [String|File] attachment A file object for attaching as an attachment.
393
+ # @param [String] filename The filename you wish the attachment to be.
394
+ # @return [void]
395
+ #
396
+ # Returns nothing
397
+ def add_file(disposition, filedata, filename)
398
+ attachment = File.open(filedata, 'r') if filedata.is_a?(String)
399
+ attachment = filedata.dup unless attachment
400
+
401
+ fail(Mailgun::ParameterError,
402
+ 'Unable to access attachment file object.'
403
+ ) unless attachment.respond_to?(:read)
404
+
405
+ unless filename.nil?
406
+ attachment.instance_variable_set :@original_filename, filename
407
+ attachment.instance_eval 'def original_filename; @original_filename; end'
408
+ end
409
+ set_multi_complex(disposition, attachment)
410
+ end
411
+ end
412
+
413
+ end