mailgun-ruby 1.1.2 → 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
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