mailgun-ruby 1.1.2 → 1.2.4

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 (46) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-env.yml.example +1 -1
  3. data/.travis.yml +8 -5
  4. data/Gemfile +1 -1
  5. data/README.md +77 -9
  6. data/{Domains.md → docs/Domains.md} +18 -0
  7. data/{Events.md → docs/Events.md} +0 -0
  8. data/{MessageBuilder.md → docs/MessageBuilder.md} +24 -5
  9. data/{Messages.md → docs/Messages.md} +3 -3
  10. data/{OptInHandler.md → docs/OptInHandler.md} +0 -0
  11. data/{Snippets.md → docs/Snippets.md} +21 -2
  12. data/docs/Suppressions.md +82 -0
  13. data/{Webhooks.md → docs/Webhooks.md} +1 -1
  14. data/docs/railgun/Overview.md +11 -0
  15. data/docs/railgun/Parameters.md +83 -0
  16. data/lib/mailgun/address.rb +5 -2
  17. data/lib/mailgun/client.rb +39 -8
  18. data/lib/mailgun/events/events.rb +40 -12
  19. data/lib/mailgun/messages/batch_message.rb +3 -2
  20. data/lib/mailgun/messages/message_builder.rb +99 -26
  21. data/lib/mailgun/suppressions.rb +273 -0
  22. data/lib/mailgun/version.rb +1 -1
  23. data/lib/mailgun/webhooks/webhooks.rb +1 -1
  24. data/lib/mailgun-ruby.rb +2 -1
  25. data/lib/railgun/attachment.rb +56 -0
  26. data/lib/railgun/errors.rb +27 -0
  27. data/lib/railgun/mailer.rb +237 -0
  28. data/lib/railgun/message.rb +17 -0
  29. data/lib/railgun/railtie.rb +10 -0
  30. data/lib/railgun.rb +8 -0
  31. data/mailgun.gemspec +12 -12
  32. data/spec/integration/email_validation_spec.rb +14 -0
  33. data/spec/integration/events_spec.rb +9 -1
  34. data/spec/integration/mailgun_spec.rb +0 -0
  35. data/spec/integration/suppressions_spec.rb +142 -0
  36. data/spec/spec_helper.rb +3 -1
  37. data/spec/unit/events/events_spec.rb +36 -2
  38. data/spec/unit/messages/batch_message_spec.rb +1 -0
  39. data/spec/unit/messages/message_builder_spec.rb +95 -19
  40. data/spec/unit/messages/sample_data/unknown.type +0 -0
  41. data/spec/unit/railgun/content_type_spec.rb +71 -0
  42. data/spec/unit/railgun/mailer_spec.rb +242 -0
  43. data/vcr_cassettes/email_validation.yml +57 -9
  44. data/vcr_cassettes/events.yml +48 -1
  45. data/vcr_cassettes/suppressions.yml +727 -0
  46. metadata +68 -36
@@ -11,6 +11,7 @@ module Mailgun
11
11
  #
12
12
  # See the Github documentation for full examples.
13
13
  class Events
14
+ include Enumerable
14
15
 
15
16
  # Public: event initializer
16
17
  #
@@ -23,31 +24,47 @@ module Mailgun
23
24
  @paging_previous = nil
24
25
  end
25
26
 
26
- # Public: Issues a simple get against the client.
27
+ # Public: Issues a simple get against the client. Alias of `next`.
27
28
  #
28
29
  # params - a Hash of query options and/or filters.
29
30
  #
30
31
  # Returns a Mailgun::Response object.
31
32
  def get(params = nil)
32
- get_events(params)
33
+ self.next(params)
33
34
  end
34
35
 
35
36
  # Public: Using built in paging, obtains the next set of data.
36
37
  # If an events request hasn't been sent previously, this will send one
37
38
  # without parameters
38
39
  #
40
+ # params - a Hash of query options and/or filters.
41
+ #
39
42
  # Returns a Mailgun::Response object.
40
- def next
41
- get_events(nil, @paging_next)
43
+ def next(params = nil)
44
+ get_events(params, @paging_next)
42
45
  end
43
46
 
44
47
  # Public: Using built in paging, obtains the previous set of data.
45
48
  # If an events request hasn't been sent previously, this will send one
46
49
  # without parameters
47
50
  #
51
+ # params - a Hash of query options and/or filters.
52
+ #
48
53
  # Returns Mailgun::Response object.
49
- def previous
50
- get_events(nil, @paging_previous)
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
51
68
  end
52
69
 
53
70
  private
@@ -70,12 +87,23 @@ module Mailgun
70
87
  #
71
88
  # Return is irrelevant.
72
89
  def extract_paging(response)
73
- # This is pretty hackish. But the URL will never change in API v2.
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
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[/\/v[\d]\/#{@domain}\/events\/(.+)/,1]
105
+ rescue URI::InvalidURIError
106
+ nil
79
107
  end
80
108
 
81
109
  # Internal: construct the event path to be used by the client
@@ -48,7 +48,7 @@ module Mailgun
48
48
  send_message if @counters[:recipients][recipient_type] == Mailgun::Chains::MAX_RECIPIENTS
49
49
 
50
50
  compiled_address = parse_address(address, variables)
51
- complex_setter(recipient_type, compiled_address)
51
+ set_multi_complex(recipient_type, compiled_address)
52
52
 
53
53
  store_recipient_variables(recipient_type, address, variables) if recipient_type != :from
54
54
 
@@ -84,7 +84,7 @@ module Mailgun
84
84
  # @return [Boolean]
85
85
  def send_message
86
86
  rkey = 'recipient-variables'
87
- simple_setter rkey, JSON.generate(@recipient_variables)
87
+ set_multi_simple rkey, JSON.generate(@recipient_variables)
88
88
  @message[rkey] = @message[rkey].first if @message.key?(rkey)
89
89
 
90
90
  response = @client.send_message(@domain, @message).to_h!
@@ -111,6 +111,7 @@ module Mailgun
111
111
  # This method resets the message object to prepare for the next batch
112
112
  # of recipients.
113
113
  def reset_message
114
+ @recipient_variables = {}
114
115
  @message.delete('recipient-variables')
115
116
  @message.delete(:to)
116
117
  @message.delete(:cc)
@@ -1,3 +1,4 @@
1
+ require 'mime/types'
1
2
  require 'time'
2
3
 
3
4
  module Mailgun
@@ -23,17 +24,24 @@ module Mailgun
23
24
 
24
25
  # Adds a specific type of recipient to the message object.
25
26
  #
27
+ # WARNING: Setting 'h:reply-to' with add_recipient() is deprecated! Use 'reply_to' instead.
28
+ #
26
29
  # @param [String] recipient_type The type of recipient. "to", "cc", "bcc" or "h:reply-to".
27
30
  # @param [String] address The email address of the recipient to add to the message object.
28
31
  # @param [Hash] variables A hash of the variables associated with the recipient. We recommend "first" and "last" at a minimum!
29
32
  # @return [void]
30
33
  def add_recipient(recipient_type, address, variables = nil)
34
+ if recipient_type == "h:reply-to"
35
+ warn 'DEPRECATION: "add_recipient("h:reply-to", ...)" is deprecated. Please use "reply_to" instead.'
36
+ return reply_to(address, variables)
37
+ end
38
+
31
39
  if (@counters[:recipients][recipient_type] || 0) >= Mailgun::Chains::MAX_RECIPIENTS
32
40
  fail Mailgun::ParameterError, 'Too many recipients added to message.', address
33
41
  end
34
42
 
35
43
  compiled_address = parse_address(address, variables)
36
- complex_setter(recipient_type, compiled_address)
44
+ set_multi_complex(recipient_type, compiled_address)
37
45
 
38
46
  @counters[:recipients][recipient_type] += 1 if @counters[:recipients].key?(recipient_type)
39
47
  end
@@ -53,12 +61,25 @@ module Mailgun
53
61
  from(address, variables)
54
62
  end
55
63
 
64
+ # Set the message's Reply-To address.
65
+ #
66
+ # Rationale: According to RFC, only one Reply-To address is allowed, so it
67
+ # is *okay* to bypass the set_multi_simple and set reply-to directly.
68
+ #
69
+ # @param [String] address The email address to provide as Reply-To.
70
+ # @param [Hash] variables A hash of variables associated with the recipient.
71
+ # @return [void]
72
+ def reply_to(address, variables = nil)
73
+ compiled_address = parse_address(address, variables)
74
+ header("reply-to", compiled_address)
75
+ end
76
+
56
77
  # Set a subject for the message object
57
78
  #
58
79
  # @param [String] subject The subject for the email.
59
80
  # @return [void]
60
81
  def subject(subj = nil)
61
- simple_setter(:subject, subj)
82
+ set_multi_simple(:subject, subj)
62
83
  end
63
84
 
64
85
  # Deprecated: Please use "subject" instead.
@@ -72,7 +93,7 @@ module Mailgun
72
93
  # @param [String] text_body The text body for the email.
73
94
  # @return [void]
74
95
  def body_text(text_body = nil)
75
- simple_setter(:text, text_body)
96
+ set_multi_simple(:text, text_body)
76
97
  end
77
98
 
78
99
  # Deprecated: Please use "body_text" instead.
@@ -86,7 +107,7 @@ module Mailgun
86
107
  # @param [String] html_body The html body for the email.
87
108
  # @return [void]
88
109
  def body_html(html_body = nil)
89
- simple_setter(:html, html_body)
110
+ set_multi_simple(:html, html_body)
90
111
  end
91
112
 
92
113
  # Deprecated: Please use "body_html" instead.
@@ -97,7 +118,7 @@ module Mailgun
97
118
 
98
119
  # Adds a series of attachments, when called upon.
99
120
  #
100
- # @param [String] attachment A file object for attaching as an attachment.
121
+ # @param [String|File] attachment A file object for attaching as an attachment.
101
122
  # @param [String] filename The filename you wish the attachment to be.
102
123
  # @return [void]
103
124
  def add_attachment(attachment, filename = nil)
@@ -106,19 +127,27 @@ module Mailgun
106
127
 
107
128
  # Adds an inline image to the mesage object.
108
129
  #
109
- # @param [String] inline_image A file object for attaching an inline image.
130
+ # @param [String|File] inline_image A file object for attaching an inline image.
110
131
  # @param [String] filename The filename you wish the inline image to be.
111
132
  # @return [void]
112
133
  def add_inline_image(inline_image, filename = nil)
113
134
  add_file(:inline, inline_image, filename)
114
135
  end
115
136
 
137
+ # Adds a List-Unsubscribe for the message header.
138
+ #
139
+ # @param [Array<String>] *variables Any number of url or mailto
140
+ # @return [void]
141
+ def list_unsubscribe(*variables)
142
+ set_single('h:List-Unsubscribe', variables.map { |var| "<#{var}>" }.join(','))
143
+ end
144
+
116
145
  # Send a message in test mode. (The message won't really be sent to the recipient)
117
146
  #
118
147
  # @param [Boolean] mode The boolean or string value (will fix itself)
119
148
  # @return [void]
120
149
  def test_mode(mode)
121
- simple_setter('o:testmode', bool_lookup(mode))
150
+ set_multi_simple('o:testmode', bool_lookup(mode))
122
151
  end
123
152
 
124
153
  # Deprecated: 'set_test_mode' is depreciated. Please use 'test_mode' instead.
@@ -132,7 +161,7 @@ module Mailgun
132
161
  # @param [Boolean] mode The boolean or string value(will fix itself)
133
162
  # @return [void]
134
163
  def dkim(mode)
135
- simple_setter('o:dkim', bool_lookup(mode))
164
+ set_multi_simple('o:dkim', bool_lookup(mode))
136
165
  end
137
166
 
138
167
  # Deprecated: 'set_dkim' is deprecated. Please use 'dkim' instead.
@@ -148,7 +177,7 @@ module Mailgun
148
177
  def add_campaign_id(campaign_id)
149
178
  fail(Mailgun::ParameterError, 'Too many campaigns added to message.', campaign_id) if @counters[:attributes][:campaign_id] >= Mailgun::Chains::MAX_CAMPAIGN_IDS
150
179
 
151
- complex_setter('o:campaign', campaign_id)
180
+ set_multi_complex('o:campaign', campaign_id)
152
181
  @counters[:attributes][:campaign_id] += 1
153
182
  end
154
183
 
@@ -160,7 +189,7 @@ module Mailgun
160
189
  if @counters[:attributes][:tag] >= Mailgun::Chains::MAX_TAGS
161
190
  fail Mailgun::ParameterError, 'Too many tags added to message.', tag
162
191
  end
163
- complex_setter('o:tag', tag)
192
+ set_multi_complex('o:tag', tag)
164
193
  @counters[:attributes][:tag] += 1
165
194
  end
166
195
 
@@ -169,7 +198,7 @@ module Mailgun
169
198
  # @param [Boolean] tracking Boolean true or false.
170
199
  # @return [void]
171
200
  def track_opens(mode)
172
- simple_setter('o:tracking-opens', bool_lookup(mode))
201
+ set_single('o:tracking-opens', bool_lookup(mode))
173
202
  end
174
203
 
175
204
  # Deprecated: 'set_open_tracking' is deprecated. Please use 'track_opens' instead.
@@ -183,7 +212,7 @@ module Mailgun
183
212
  # @param [String] mode True, False, or HTML (for HTML only tracking)
184
213
  # @return [void]
185
214
  def track_clicks(mode)
186
- simple_setter('o:tracking-clicks', bool_lookup(mode))
215
+ set_single('o:tracking-clicks', bool_lookup(mode))
187
216
  end
188
217
 
189
218
  # Depreciated: 'set_click_tracking. is deprecated. Please use 'track_clicks' instead.
@@ -201,7 +230,7 @@ module Mailgun
201
230
  # @return [void]
202
231
  def deliver_at(timestamp)
203
232
  time_str = DateTime.parse(timestamp)
204
- simple_setter('o:deliverytime', time_str.rfc2822)
233
+ set_multi_simple('o:deliverytime', time_str.rfc2822)
205
234
  end
206
235
 
207
236
  # Deprecated: 'set_delivery_time' is deprecated. Please use 'deliver_at' instead.
@@ -218,8 +247,12 @@ module Mailgun
218
247
  # @return [void]
219
248
  def header(name, data)
220
249
  fail(Mailgun::ParameterError, 'Header name for message must be specified') if name.to_s.empty?
221
- jsondata = make_json data
222
- simple_setter("v:#{name}", jsondata)
250
+ begin
251
+ jsondata = make_json data
252
+ set_single("h:#{name}", jsondata)
253
+ rescue Mailgun::ParameterError
254
+ set_single("h:#{name}", data)
255
+ end
223
256
  end
224
257
 
225
258
  # Deprecated: 'set_custom_data' is deprecated. Please use 'header' instead.
@@ -228,6 +261,23 @@ module Mailgun
228
261
  header name, data
229
262
  end
230
263
 
264
+ # Attaches custom JSON data to the message. See the following doc page for more info.
265
+ # https://documentation.mailgun.com/user_manual.html#attaching-data-to-messages
266
+ #
267
+ # @param [String] name A name for the custom variable block.
268
+ # @param [String|Hash] data Either a string or a hash. If it is not valid JSON or
269
+ # can not be converted to JSON, ParameterError will be raised.
270
+ # @return [void]
271
+ def variable(name, data)
272
+ fail(Mailgun::ParameterError, 'Variable name must be specified') if name.to_s.empty?
273
+ begin
274
+ jsondata = make_json data
275
+ set_single("v:#{name}", jsondata)
276
+ rescue Mailgun::ParameterError
277
+ set_single("v:#{name}", data)
278
+ end
279
+ end
280
+
231
281
  # Add custom parameter to the message. A custom parameter is any parameter that
232
282
  # is not yet supported by the SDK, but available at the API. Note: No validation
233
283
  # is performed. Don't forget to prefix the parameter with o, h, or v.
@@ -236,7 +286,7 @@ module Mailgun
236
286
  # @param [string] data A string of data for the parameter.
237
287
  # @return [void]
238
288
  def add_custom_parameter(name, data)
239
- complex_setter(name, data)
289
+ set_multi_complex(name, data)
240
290
  end
241
291
 
242
292
  # Set the Message-Id header to a custom value. Don't forget to enclose the
@@ -249,7 +299,7 @@ module Mailgun
249
299
  def message_id(data = nil)
250
300
  key = 'h:Message-Id'
251
301
  return @message.delete(key) if data.to_s.empty?
252
- @message[key] = data
302
+ set_single(key, data)
253
303
  end
254
304
 
255
305
  # Deprecated: 'set_message_id' is deprecated. Use 'message_id' instead.
@@ -260,13 +310,23 @@ module Mailgun
260
310
 
261
311
  private
262
312
 
313
+ # Sets a single value in the message hash where "multidict" features are not needed.
314
+ # Does *not* permit duplicate params.
315
+ #
316
+ # @param [String] parameter The message object parameter name.
317
+ # @param [String] value The value of the parameter.
318
+ # @return [void]
319
+ def set_single(parameter, value)
320
+ @message[parameter] = value ? value : ''
321
+ end
322
+
263
323
  # Sets values within the multidict, however, prevents
264
324
  # duplicate values for keys.
265
325
  #
266
326
  # @param [String] parameter The message object parameter name.
267
327
  # @param [String] value The value of the parameter.
268
328
  # @return [void]
269
- def simple_setter(parameter, value)
329
+ def set_multi_simple(parameter, value)
270
330
  @message[parameter] = value ? [value] : ['']
271
331
  end
272
332
 
@@ -276,7 +336,7 @@ module Mailgun
276
336
  # @param [String] parameter The message object parameter name.
277
337
  # @param [String] value The value of the parameter.
278
338
  # @return [void]
279
- def complex_setter(parameter, value)
339
+ def set_multi_complex(parameter, value)
280
340
  @message[parameter] << (value || '')
281
341
  end
282
342
 
@@ -307,9 +367,9 @@ module Mailgun
307
367
  #
308
368
  # Returns a JSON object or raises ParameterError
309
369
  def make_json(obj)
310
- return JSON.parse(obj).to_s if obj.is_a?(String)
311
- return obj.to_s if obj.is_a?(Hash)
312
- return JSON.generate(obj).to_s
370
+ return JSON.parse(obj).to_json if obj.is_a?(String)
371
+ return obj.to_json if obj.is_a?(Hash)
372
+ return JSON.generate(obj).to_json
313
373
  rescue
314
374
  raise Mailgun::ParameterError, 'Provided data could not be made into JSON. Try a JSON string or Hash.', obj
315
375
  end
@@ -324,17 +384,24 @@ module Mailgun
324
384
  def parse_address(address, vars)
325
385
  return address unless vars.is_a? Hash
326
386
  fail(Mailgun::ParameterError, 'Email address not specified') unless address.is_a? String
387
+ if vars['full_name'] != nil && (vars['first'] != nil || vars['last'] != nil)
388
+ fail(Mailgun::ParameterError, 'Must specify at most one of full_name or first/last. Vars passed: #{vars}')
389
+ end
327
390
 
328
- full_name = "#{vars['first']} #{vars['last']}".strip
391
+ if vars['full_name']
392
+ full_name = vars['full_name']
393
+ elsif vars['first'] || vars['last']
394
+ full_name = "#{vars['first']} #{vars['last']}".strip
395
+ end
329
396
 
330
- return "'#{full_name}' <#{address}>" if defined?(full_name)
397
+ return "'#{full_name}' <#{address}>" if full_name
331
398
  address
332
399
  end
333
400
 
334
401
  # Private: Adds a file to the message.
335
402
  #
336
403
  # @param [Symbol] disposition The type of file: :attachment or :inline
337
- # @param [String] attachment A file object for attaching as an attachment.
404
+ # @param [String|File] attachment A file object for attaching as an attachment.
338
405
  # @param [String] filename The filename you wish the attachment to be.
339
406
  # @return [void]
340
407
  #
@@ -347,11 +414,17 @@ module Mailgun
347
414
  'Unable to access attachment file object.'
348
415
  ) unless attachment.respond_to?(:read)
349
416
 
417
+ if attachment.respond_to?(:path) && !attachment.respond_to?(:content_type)
418
+ mime_types = MIME::Types.type_for(attachment.path)
419
+ content_type = mime_types.empty? ? 'application/octet-stream' : mime_types[0].content_type
420
+ attachment.instance_eval "def content_type; '#{content_type}'; end"
421
+ end
422
+
350
423
  unless filename.nil?
351
424
  attachment.instance_variable_set :@original_filename, filename
352
425
  attachment.instance_eval 'def original_filename; @original_filename; end'
353
426
  end
354
- complex_setter(disposition, attachment)
427
+ set_multi_complex(disposition, attachment)
355
428
  end
356
429
  end
357
430